Go Wiki:WebAssembly

引言

Go 1.11 增加了一个实验性的 WebAssembly 端口。Go 1.12 对其部分功能进行了改进,Go 1.13 预计会有进一步的改进。Go 1.21 增加了一个针对 WASI 系统调用 API 的新端口。

WebAssembly 在其主页上被描述为

WebAssembly(缩写为 Wasm)是一种用于基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为高级语言(如 C/C++/Rust)编译的可移植目标,支持在 Web 上部署客户端和服务器应用程序。


如果您是 WebAssembly 的新手,请阅读入门部分,观看一些Go WebAssembly 演讲,然后查看下面的更多示例


JavaScript (GOOS=js) 端口

入门

本页面假设您已安装功能正常的 Go 1.11 或更新版本。有关故障排除,请参阅安装故障排除页面。

如果您使用的是 Windows,我们建议您使用 Git Bash 等 BASH 仿真系统来遵循本教程。

对于 Go 1.23 及更早版本,本文所需的 wasm 支持文件位于 misc/wasm 中,并且在执行诸如 lib/wasm/wasm_exec.js 之类的文件操作时应替换路径。

编译一个基本的 Go 包以用于 Web

package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}

设置 GOOS=jsGOARCH=wasm 环境变量以编译 WebAssembly

$ GOOS=js GOARCH=wasm go build -o main.wasm

这将构建包并生成一个名为 main.wasm 的可执行 WebAssembly 模块文件。 .wasm 文件扩展名将使其更容易在后续通过 HTTP 以正确的 Content-Type 标头提供服务。

请注意,您只能编译主包。否则,您将获得一个无法在 WebAssembly 中运行的目标文件。如果您有一个想要与 WebAssembly 一起使用的包,请将其转换为一个主包并构建一个二进制文件。

要在浏览器中执行 main.wasm,我们还需要一个 JavaScript 支持文件和一个 HTML 页面来将所有内容连接在一起。

复制 JavaScript 支持文件

cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .

创建一个 index.html 文件

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                go.run(result.instance);
            });
        </script>
    </head>
    <body></body>
</html>

如果您的浏览器尚不支持 WebAssembly.instantiateStreaming,您可以使用polyfill

然后从 Web 服务器提供这三个文件(index.htmlwasm_exec.jsmain.wasm)。例如,使用goexec

# install goexec: go install github.com/shurcooL/goexec@latest
goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'

或者使用您自己的基本 HTTP 服务器命令

注意:编译器和 wasm_exec.js 支持文件必须使用相同的 Go 主要版本。也就是说,如果 main.wasm 文件是使用 Go 1.N 版本编译的,则相应的 wasm_exec.js 文件也必须从 Go 1.N 版本复制。不支持其他组合。

注意:对于 goexec 命令在类 Unix 系统上工作,您必须将 Go 的路径环境变量添加到 shell 的 profile 中。Go 的入门指南对此进行了解释

将 /usr/local/go/bin 添加到 PATH 环境变量中。您可以通过将此行添加到 /etc/profile(对于系统范围安装)或 $HOME/.profile 来实现

export PATH=$PATH:/usr/local/go/bin

注意:对配置文件所做的更改可能要等到下次登录计算机时才会生效

最后,导航到 https://:8080/index.html,打开 JavaScript 调试控制台,您应该会看到输出。您可以修改程序,重新构建 main.wasm,然后刷新以查看新输出。

使用 Node.js 执行 WebAssembly

可以使用 Node.js 而不是浏览器来执行编译后的 WebAssembly 模块,这对于测试和自动化很有用。

首先,请确保已安装 Node 并且它在您的 PATH 中。

然后,将 $(go env GOROOT)/lib/wasm 添加到您的 PATH 中。这将允许 go rungo testPATH 搜索中找到 go_js_wasm_exec 并使用它来直接用于 js/wasm

$ export PATH="$PATH:$(go env GOROOT)/lib/wasm"
$ GOOS=js GOARCH=wasm go run .
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test
PASS
ok      example.org/my/pkg  0.800s

如果您正在 Go 本身工作,这还将允许您无缝运行 run.bash

go_js_wasm_exec 是一个包装器,允许在 Node 中运行 Go Wasm 二进制文件。默认情况下,它可以在 Go 安装的 lib/wasm 目录中找到。

如果您不想将任何内容添加到 PATH 中,您也可以在手动执行 go rungo test 时,将 -exec 标志设置为 go_js_wasm_exec 的位置。

$ GOOS=js GOARCH=wasm go run -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" .
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec"
PASS
ok      example.org/my/pkg  0.800s

最后,包装器也可以直接执行 Go Wasm 二进制文件

$ GOOS=js GOARCH=wasm go build -o mybin .
$ $(go env GOROOT)/lib/wasm/go_js_wasm_exec ./mybin
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test -c
$ $(go env GOROOT)/lib/wasm/go_js_wasm_exec ./pkg.test
PASS
ok      example.org/my/pkg  0.800s

在浏览器中运行测试

您还可以使用 wasmbrowsertest 在浏览器中运行测试。它自动化了启动 Web 服务器的工作,并使用无头 Chrome 在其中运行测试并将日志中继到您的控制台。

和以前一样,只需 go get github.com/agnivade/wasmbrowsertest 即可获取二进制文件。将其重命名为 go_js_wasm_exec 并将其放置到您的 PATH

$ mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec
$ export PATH="$PATH:$GOPATH/bin"
$ GOOS=js GOARCH=wasm go test
PASS
ok      example.org/my/pkg  0.800s

或者,使用 exec 测试标志。

GOOS=js GOARCH=wasm go test -exec="$GOPATH/bin/wasmbrowsertest"

与 DOM 交互

请参阅https://pkg.go.dev/syscall/js

另外

  • app:一个兼容 PWA、基于 React 的框架,带有自定义工具。

  • dom:一个用于简化 DOM 操作的库正在开发中。

  • dom:JavaScript DOM API 的 Go 绑定。

  • domui:一个纯 Go 框架,用于创建完整的 GUI 应用程序。

  • gas:用于 WebAssembly 应用程序的基于组件的框架。

  • GoWebian:一个用纯 Go 构建页面并添加 WebAssembly 绑定的库。

  • hogusuru:一个高级 webassembly 框架,直接在 GO 中实现了浏览器的大部分功能(包括 indexeddb、serviceworker、websocket 等)。

  • VECTY:使用 WebAssembly 在 Go 中构建响应式和动态 Web 前端,与 React 和 VueJS 等现代 Web 框架竞争。

  • vert:Go 和 JS 值之间的 WebAssembly 互操作。

  • vue:用于 WebAssembly 应用程序的渐进式框架。

  • Vugu:一个 wasm Web UI 库,具有 HTML 布局和 Go 应用程序逻辑、单文件组件、快速开发和原型设计工作流程。

  • webapi:DOM、HTML、WebGL 等的绑定生成器和生成的绑定。

  • webgen:在 HTML 中定义组件,并使用 webapi 为它们生成 Go 类型和构造函数。

Canvas

使用 net/http 配置 fetch 选项

您可以使用 net/http 库从 Go 发出 HTTP 请求,它们将被转换为 fetch 调用。但是,fetch 选项和 http 客户端选项之间没有直接映射。为了实现这一点,我们有一些特殊的标头值被识别为 fetch 选项。它们是 -

  • js.fetch:mode:Fetch API 模式设置的一个选项。有效值为:“cors”、“no-cors”、“same-origin”、“navigate”。默认值为“same-origin”。

  • js.fetch:credentials:Fetch API 凭据设置的一个选项。有效值为:“omit”、“same-origin”、“include”。默认值为“same-origin”。

  • js.fetch:redirect:Fetch API 重定向设置的一个选项。有效值为:“follow”、“error”、“manual”。默认值为“follow”。

因此,举例来说,如果我们想在发出请求时将模式设置为“cors”,它将是这样的

req, err := http.NewRequest("GET", "https://:8080", nil)
req.Header.Add("js.fetch:mode", "cors")
if err != nil {
  fmt.Println(err)
  return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
  fmt.Println(err)
  return
}
defer resp.Body.Close()
// handle the response

请随时订阅#26769以获取更多上下文和可能更新的信息。

Chrome 中的 WebAssembly

如果您运行较新版本的 Chrome,有一个标志 (chrome://flags/#enable-webassembly-baseline) 可以启用 Liftoff,即他们的新编译器,这应该会显著缩短加载时间。更多信息请参见此处

更多示例

通用

Canvas (2D)

数据库

  • TiDB-Wasm - 在浏览器中运行 TiDB(一个 golang 数据库)在 Wasm 上。

WebGL canvas (3D)

WASI (GOOS=wasip1) 端口

入门 (WASI)

Go 1.21 引入了 WASI 作为支持的平台。要为 WASI 构建,请使用 wasip1 端口

$ GOOS=wasip1 GOARCH=wasm go build -o main.wasm

官方博客有一篇关于使用 WASI 端口的有用介绍:https://go-lang.org.cn/blog/wasi

Go WebAssembly 演讲

编辑器配置

调试

WebAssembly 尚不支持调试器,因此您现在需要使用传统的 println() 方法在 JavaScript 控制台上显示输出。

一个官方的 WebAssembly 调试小组已经成立,旨在解决这个问题,一些初步的调查和提案正在进行中

如果您对调试器方面感兴趣,请参与并帮助推动这项工作。:smile

分析 WebAssembly 文件的结构

WebAssembly Code Explorer 对于可视化 WebAssembly 文件的结构很有用。

  • 点击左侧的十六进制值将突出显示其所属的部分,以及右侧的相应文本表示
  • 点击右侧的一行将突出显示左侧的十六进制字节表示

减小 Wasm 文件的大小

目前,Go 生成的 Wasm 文件很大,最小可能的大小约为 2MB。如果您的 Go 代码导入库,此文件大小可能会急剧增加。超过 10MB 很常见。

目前(暂时)有两种主要方法可以减小此文件大小

  1. 手动压缩 .wasm 文件。

    • 使用 gz 压缩将约 2MB(最小文件大小)的 WASM 示例文件减小到约 500kB。最好使用 Zopfli 进行 gzip 压缩,因为它比 gzip --best 效果更好,但运行时间也更长。
    • 使用 Brotli 进行压缩,文件大小明显优于 Zopfli 和 gzip --best,并且压缩时间介于两者之间。这个 (新的)Brotli 压缩器看起来不错。

    来自 @johanbrandhorst 的示例

    示例 1

    大小 命令 压缩时间
    16M (未压缩大小) 不适用
    2.4M brotli -o test.wasm.br test.wasm 53.6s
    3.3M go-zopfli test.wasm 3m 2.6s
    3.4M gzip --best test.wasm 2.5s
    3.4M gzip test.wasm 0.8s

    示例 2

    大小 命令 压缩时间
    2.3M (未压缩大小) 不适用
    496K brotli -o main.wasm.br main.wasm 5.7s
    640K go-zopfli main.wasm 16.2s
    660K gzip --best main.wasm 0.2s
    668K gzip main.wasm 0.2s

    使用 https://github.com/lpar/gzipped 之类的工具,在可用时自动提供带有正确标头的压缩文件。

  2. 改用 TinyGo 生成 Wasm 文件。

    TinyGo 支持 Go 语言的一个子集,目标是嵌入式设备,并有一个 WebAssembly 输出目标。

    虽然它确实有局限性(尚未完全实现 Go),但它仍然相当强大,并且生成的 Wasm 文件……很小。大约 10kB 并不罕见。“Hello world”示例只有 575 字节。如果您对其进行 gz -6 压缩,它会下降到 408 字节。:wink

    该项目也在积极开发中,因此其功能正在迅速扩展。有关使用 TinyGo 和 WebAssembly 的更多信息,请参见 https://tinygo.org/docs/guides/webassembly/

其他 WebAssembly 资源

  • Awesome-Wasm - 大量其他 Wasm 资源的列表。非 Go 特定。

此内容是 Go Wiki 的一部分。