Go 官方博客

使用 Go 构建可扩展的 Wasm 应用

Cherry Mui
2025 年 2 月 13 日

Go 1.24 通过新增 go:wasmexport 指令以及构建 WebAssembly 系统接口 (WASI) Reactor 的能力,增强了其 WebAssembly (Wasm) 功能。这些特性使得 Go 开发者能够将 Go 函数导出到 Wasm,从而更好地与 Wasm 主机集成,并扩展了基于 Go 的 Wasm 应用的可能性。

WebAssembly 和 WebAssembly 系统接口

WebAssembly (Wasm) 是一种二进制指令格式,最初是为 Web 浏览器创建的,它提供了接近原生性能的高性能、低级代码执行能力。自那时起,Wasm 的用途不断扩展,现在已被用于浏览器之外的各种环境。值得注意的是,云提供商提供了直接执行 Wasm 可执行文件的服务,这利用了 WebAssembly 系统接口 (WASI) 系统调用 API。WASI 允许这些可执行文件与系统资源交互。

Go 最早在 1.11 版本中通过 js/wasm 移植实现了对编译到 Wasm 的支持。Go 1.21 通过新的 GOOS=wasip1 移植增加了针对 WASI preview 1 系统调用 API 的新移植。

使用 go:wasmexport 将 Go 函数导出到 Wasm

Go 1.24 引入了一个新的编译器指令 go:wasmexport,它允许开发者将 Go 函数导出,以便从 Wasm 模块外部调用,通常是从运行 Wasm 运行时的主机应用程序调用。此指令指示编译器将带有该注解的函数作为 Wasm 导出 在生成的 Wasm 二进制文件中可用。

要使用 go:wasmexport 指令,只需将其添加到函数定义中

//go:wasmexport add
func add(a, b int32) int32 { return a + b }

这样,Wasm 模块将有一个名为 add 的导出函数,可以从主机调用。

这类似于 cgo 的 export 指令,后者使函数可以从 C 调用,但 go:wasmexport 使用了一种不同且更简单的机制。

构建 WASI Reactor

WASI Reactor 是一种持续运行的 WebAssembly 模块,可以多次调用以响应事件或请求。与主函数完成后终止的“command”模块不同,Reactor 实例在初始化后保持活动状态,并且其导出函数仍然可访问。

使用 Go 1.24,可以通过 -buildmode=c-shared 构建标志构建 WASI Reactor。

$ GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm

该构建标志指示链接器不生成 _start 函数(命令模块的入口点),而是生成一个 _initialize 函数,该函数执行运行时和包的初始化,以及任何导出的函数及其依赖项。在调用任何其他导出函数之前,必须调用 _initialize 函数。main 函数将不会自动调用。

要使用 WASI Reactor,主机应用程序首先通过调用 _initialize 来初始化它,然后只需调用导出的函数即可。以下是使用基于 Go 的 Wasm 运行时实现 Wazero 的示例

// Create a Wasm runtime, set up WASI.
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)

// Configure the module to initialize the reactor.
config := wazero.NewModuleConfig().WithStartFunctions("_initialize")

// Instantiate the module.
wasmModule, _ := r.InstantiateWithConfig(ctx, wasmFile, config)

// Call the exported function.
fn := wasmModule.ExportedFunction("add")
var a, b int32 = 1, 2
res, _ := fn.Call(ctx, api.EncodeI32(a), api.EncodeI32(b))
c := api.DecodeI32(res[0])
fmt.Printf("add(%d, %d) = %d\n", a, b, c)

// The instance is still alive. We can call the function again.
res, _ = fn.Call(ctx, api.EncodeI32(b), api.EncodeI32(c))
fmt.Printf("add(%d, %d) = %d\n", b, c, api.DecodeI32(res[0]))

go:wasmexport 指令和 Reactor 构建模式允许应用程序通过调用基于 Go 的 Wasm 代码进行扩展。这对于采用 Wasm 作为具有明确接口的插件或扩展机制的应用程序来说尤其有价值。通过导出 Go 函数,应用程序可以利用 Go Wasm 模块提供功能,而无需重新编译整个应用程序。此外,构建为 Reactor 可确保导出的函数可以多次调用而无需重新初始化,这使其适用于长时间运行的应用程序或服务。

支持主机和客户端之间的丰富类型

Go 1.24 还放宽了 go:wasmimport 函数可用作输入和结果参数的类型限制。例如,可以传递布尔值、字符串、指向 int32 的指针,或者指向嵌入 structs.HostLayout 并包含支持的字段类型的结构体的指针(详情请参见文档)。这使得 Go Wasm 应用能够以更自然、更符合人体工程学的方式编写,并消除了一些不必要的类型转换。

限制

尽管 Go 1.24 在其 Wasm 功能方面取得了显著增强,但仍然存在一些明显的限制。

Wasm 是一种单线程架构,没有并行性。go:wasmexport 函数可以派生新的 goroutine。但是,如果函数创建了后台 goroutine,当 go:wasmexport 函数返回时,该 goroutine 不会继续执行,除非再次调用基于 Go 的 Wasm 模块。

尽管 Go 1.24 放宽了一些类型限制,但 go:wasmimportgo:wasmexport 函数可用的类型仍然存在限制。由于客户端的 64 位架构和主机的 32 位架构之间不幸的失配,无法在内存中传递指针。例如,go:wasmimport 函数不能接受指向包含指针类型字段的结构体的指针。

结论

Go 1.24 中新增的构建 WASI Reactor 和将 Go 函数导出到 Wasm 的能力,标志着 Go 在 WebAssembly 功能方面迈出了重要一步。这些特性赋予开发者创建更通用、更强大的基于 Go 的 Wasm 应用的能力,为 Go 在 Wasm 生态系统中开辟了新的可能性。

下一篇文章:使用 testing/synctest 测试并发代码
上一篇文章:Go 1.24 发布啦!
博客索引