一、简介
Go 语言在前几个月前发布了 1.11 版本,个人感觉影响最大的除了 Go Module 外就是支持 WebAssembly 了,正好 Go 和 JS 都是我自己喜欢的语言,本文就简单介绍下如何使用 Go 语言开发编译 WebAssembly 模块,并在 Node.js 和浏览器端使用。
关于 Go 语言环境安装,请参考 GO 语言官网。
二、Hello Go WebAssembly
首先,还是最简单的 Hello World 开始,简单介绍如何将 Go 源码编译为 wasm 文件
建立 main.go 文件,输入如下的代码:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Go WebAssembly")
}
这就是最简单 Go 源代码文件,在代码当前目录执行如下的编译命令将这段代码编译为 wasm 文件
GOARCH=wasm GOOS=js go build -o lib.wasm main.go
这段命令就是 Go 的交叉编译命令;其中 GOARCH, GOOS 分别指代编译的目标系统和平台,比如我们可以换为 GOOS=linux GOARCH=amd64
来编译为 Linux 下的 64 位可执行程序;-o 指定输出文件名。
执行完成后会在当前目录生成 WebAssembly 的二进制字节码文件 lib.wasm
。
三、浏览器端引入并执行 wasm 文件
我们先来看看浏览器端如何引入生成的 lib.wasm
文件,建立 index.html
文件,根据 Go 仓库中内容,文件如下:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Go WebAssembly</title>
</head>
<body>
<script src="./wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(async (result) => {
mod = result.module;
inst = result.instance;
await go.run(result.instance)
});
</script>
</body>
</html>
其中 wasm_exec.js
文件为官方提供的一份 Go WebAssembly 的依赖文件,请从 github.com/golang/go/b… 获取。
重点是下面这段代码
const go = new Go();
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(async (result) => {
mod = result.module;
inst = result.instance;
await go.run(result.instance)
});
其中 Go
这个全局对象是 wasm_exec.js
文件中定义的,先实例化这个对象;接着使用 fetch
方法获取到 lib.wasm
文件,然后通过 WebAssembly.instantiateStreaming
方法编译和实例化 WebAssembly 代码,其中第二个参数 go.importObject
是向 lib.wasm
代码中导入的一些运行时对象,感兴趣可以看看 wasm_exec.js
文件;最后调用 go.run(result.instance)
执行实例化的代码。
这里需要注意的是:index.html
必须放在 Web 服务器上,使用 HTTP 协议访问,所以使用 Nginx 或者 Go、Node 自己写一个小服务器都可以;还有是 lib.wasm
文件的 Content-Type 必须是 application/wasm
,所以可以在 Nginx 的配置文件 mime.types
中添加 application/wasm wasm;
这样一行,或者其他服务器使用自己的方式来实现。
例如我这里访问 http://127.0.0.1/go-web-assembly/
就可以看到执行结果了

四、Node.js 引入并调用 wasm 文件
官方的仓库提供了一种 node 命令行调用 wasm 的方式,没有提供 node 代码中调用 wasm 的方式,我简单将 wasm_exec.js
文件修改,提交了个 npm 模块 go-wasm
,使用这个模块来引用 wasm 文件
建立 index.js
文件,输入如下内容
const {Go} = require('go-wasm')
const start = async function start() {
const go = new Go()
const result = await WebAssembly.instantiate(fs.readFileSync('./lib.wasm'), go.importObject)
await go.run(result.instance)
}
start();
执行 node index.js
可以看到控制台中输出了 "Hello Go WebAssembly"。
结合上面浏览器中的输出,可以看到 Go 代码中的 fmt.Println
都会通过 console.log
出来,但其实浏览器端和 Node.js 端又是不一样的,在 wasm_exec.js
中可以看到,Go 代码中的输出会调用 runtime.wasmWrite 方法,而这个方法会从共享内存中取出数据和输出类型(标准输出还是错误输出等),在 JS 端 wasmWrite 实现中调用了 fs.writeSync
将输出数据写入对应的输出类型中,Node.js 中就是直接调用 fs 模块,而浏览器端实现了 fs.writeSync 到 console.log
中,最终输出到浏览器的开发者工具中。
五、在 Go 代码中调用 JS 变量
上面的基础代码只是介绍了 JS 中调用 wasm,并没有太多的实际用处,接下来就看看在 Go 代码和 JS 如何互相调用;
在 Go 代码中调用 JS 变量需要引入一个标准库 syscall/js
,例如如下的代码,我们可以通过 Go 代码获取 JS 中变量的值,调用 JS 中的方法
package main
import (
"syscall/js"
)
func readUserAgent() string {
return js.Global().Get("navigator").Get("userAgent").String()
}
func alert(str string) {
js.Global().Get("alert").Invoke(str)
}
func main() {
userAgent := readUserAgent()
alert(userAgent)
}
可以看到,我们通过 js.Global().Get("navigator").Get("userAgent").String()
来获取浏览器环境的 global.navigator.userAgent
属性,就是 UserAgent 字符串,然后调用 js.Global().Get("alert").Invoke(str)
来执行 alert 方法,将 UserAgent 显示出来,其中 alert 调用也可以改为
js.Global().Call("alert", str)
这种方式;
syscall/js
标准库提供了一个 js.Value
的类型,指代 Javascript 中的变量,上面的代码 js.Global()
,js.Global().Get("xxx")
等都会返回一个 js.Value
的类型,js.Value
类型拥有下面一些方法可供我们来使用:
js.Value.Get(p string) js.Value
、js.Value.Set(p string, x interface{})
这两个方法获取和设置对象的属性js.Value.Index(i int) js.Value
、js.Value.SetIndex(i int, x interface{})
、js.Value.Length() int
这三个方法分别获取和设置数组项的值,获取数组的长度js.Value.Call(m string, args ...interface{}) js.Value
调用对象的方法,args 传入调用的参数,返回函数的返回值js.Value.Invoke(args ...interface{}) js.Value
执行函数,返回函数返回值js.Value.New(args ...interface{}) js.Value
通过 new 调用函数,返回函数返回值js.Value.InstanceOf(t js.Value) bool
类似 JS 中 instanceof 操作,返回 true 或 false- 除了上面这些方法,还有
js.Value.Type()
获取 js.Value 的类型,还有js.Value.Int()
、js.Value.Bool()
、js.Value.Float()
、js.Value.String()
等分别将 js.Value 类型转换为 Go 语言变量类型,对应规则如下:
| Go | JavaScript |
| ---------------------- | ---------------------- |
| js.Value | [its value] |
| js.TypedArray | typed array |
| js.Callback | function |
| nil | null |
| bool | boolean |
| integers and floats | number |
| string | string |
| []interface{} | new array |
| map[string]interface{} | new object |
更详细的文档参考GoDoc。
六、在 JS 代码中调用 Go 变量、函数
除了在 Go 代码中调用 JS 环境的一些变量外,我们也可以将 Go 中的方法,变量导出来,在 JS 代码中调用,如下代码:
package main
import (
"syscall/js"
)
func add(i []js.Value) {
valueA := i[0].Int()
valueB := i[1].Int()
sum := valueA + valueB
js.Global().Get("console").Call("log", "sum: ", js.ValueOf(sum))
}
func main() {
c := make(chan struct{}, 0)
callback := js.NewCallback(add)
js.Global().Set("add", callback)
defer callback.Release()
<-c
}
这里使用 js.NewCallback(fn func(args []Value)) js.Callback
这个函数将 Go 函数 add
转换并添加到 JS 全局变量中去
然后我们在 JS 中就可以使用下面这段代码来调用了
<body>
<script src="./wasm_exec.js"></script>
<script>
// 省略部分代码
const go = new Go();
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(async (result) => {
await go.run(result.instance)
});
let count = 0;
const runFunction = async function runFunction() {
add(1, count++)
}
</script>
<button onClick="runFunction()" id="addButton">Add</button>
</body>
还有在 Go 代码中,我们使用了 Channel 将 main 函数阻塞住,防止 Go 代码执行结束后就退出,因为 JS 调用是在点击按钮后触发的,这时候需要保证 wasm 程序还在运行中;
JS 调用 Go 函数,不能直接获取函数的返回值,需要将返回值设置在全局变量中,而且 Go 注册到 JS 的函数是异步调用的函数,所以全局变量并不能直接同步取出,所以上面的例子是在 Go 语言中使用 console 来取巧,比较合适的例子参考下面参考文章2中的例子,带有返回值的同步函数会在 Go 1.12 版本中支持。
七、结语
WebAssembly 在性能上相比 JavaScript 能好一些,所以给一些 Web 应用提供了更大的可能性,例如以后可以在网页端使用更复杂的图片处理、视频渲染;也让 Node 端可以实现之前通过编译来实现的一些模块,比如 node-sass 这个模块就可以加载 wasm 文件来处理,省去了本地编译原生 node 模块的步骤,或者做类似验证码生成的功能,通过 wasm 文件直接调用 Go 语言等内置的图形库,补充了 Node 自身图形库缺失的问题。
WebAssembly 还可以在一些对于代码保护方面,可以将一些敏感的代码使用 wasm 文件加载,达到混淆代码的作用,提高分析代码的逻辑的门槛。
本文简单的介绍了在 JS 中调用 Go 编译的 WebAssembly 文件,Go 语言支持 WebAssembly 还处在开始阶段,许多特性还暂时不支持;后面随着 Go 版本的升级,我也会不时添加关于这个话题更多的文章。