Go 博客

使用 Go 实现可扩展的 Wasm 应用

Cherry Mui
2025 年 2 月 13 日

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

WebAssembly 和 WebAssembly System Interface

WebAssembly (Wasm) 是一种二进制指令格式,最初是为 Web 浏览器创建的,能够以接近原生性能的速度执行高性能的底层代码。自那时以来,Wasm 的应用范围已得到扩展,现在已在浏览器之外的各种环境中得到使用。值得注意的是,云服务提供商提供直接执行 Wasm 可执行文件的服务,并利用 WebAssembly System Interface (WASI) 系统调用 API。WASI 允许这些可执行文件与系统资源进行交互。

Go 在 1.11 版本中首次通过 js/wasm 端口添加了对编译到 Wasm 的支持。Go 1.21 通过新的 GOOS=wasip1 端口添加了一个针对 WASI preview 1 syscall API 的新端口。

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

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

要使用 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 函数(command 模块的入口点),而是生成一个 _initialize 函数,该函数执行运行时和包的初始化,以及任何导出函数及其依赖项。必须在调用任何其他导出函数之前调用 _initialize 函数。main 函数不会自动调用。

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

// 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 函数的输入和结果参数可以使用的类型限制。例如,您可以传递一个 bool、一个 string、一个指向 int32 的指针,或者一个指向嵌入了 structs.HostLayout 并包含支持的字段类型的结构体的指针(有关详细信息,请参阅文档)。这使得 Go Wasm 应用程序的编写方式更加自然和符合人体工程学,并消除了不必要的类型转换。

局限性

虽然 Go 1.24 对其 Wasm 功能进行了重大增强,但仍然存在一些显著的限制。

Wasm 是一个单线程架构,没有并行性。go:wasmexport 函数可以生成新的 goroutine。但是,如果一个函数创建了一个后台 goroutine,那么在 go:wasmexport 函数返回之前,它不会继续执行,直到再次调用基于 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 发布!
博客索引