在〈Go 调用 JavaScript〉看过如何在 Go 中获取 JavaScript 的函数,然后予以调用,若你曾稍微了解过〈WebAssembly〉,就会发觉,这跟 WebAssembly 导入函数至 WebAssembly 的方式不同。
这是 JavaScript 的 wasm_exec.js 以及 Go 的syscall/js
居中之缘故,在 wasm_exec.html 中你也可以看到加载、编译、实例化 WebAssembly 的过程:
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();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
});
async function run() {
console.clear();
await go.run(inst);
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
Go 有自己的导入对象,也就是go.importObject
,这个对象主要是 JavaScript 环境与 Go 编译出来的 WebAssembly 之桥梁,将 JavaScript 的值与 Go 的结构实例作了个对应,因此,不用自己导入某个函数,只要获取某个作为命名空间的 JavaScript 对象,获取上头对应的特性,像是函数,就可以在 Go 中操作。
也就是说,如果想要在 Go 中定义函数,然后在 JavaScript 中调用,就是将 Go 中定义的函数,设定给某个对应的 JavaScript 对象,之后就可以在 JavaScript 环境中使用了,只不过在定义时,必须留意 JavaScript 与 Go 的类型对应。
可以被 JavaScript 环境调用的 Go 函数,必须被包装为js.Callback
类型,这个结构类型内嵌js.Value
,也就是它也是一种值,想要创建js.Callback
实例,可以透过js.NewCallback
函数(定义在 callback.go)。
要能被 JavaScript 调用的 Go 函数,参数类型是[]js.Value
,也就是js.Value
的slice
,slice
的元素代表着调用函数时传入的实参,你可以想像 JavaScript 函数中arguments
的对应类型。
例如,显示累加至某个指定 DOM 对象的函数,可以如下定义:
package main
import "syscall/js"
func main() {
// 注册在 JavaScript 全局
js.Global().Set("printSumTo", js.NewCallback(printSum))
// 阻断 main 流程
select {}
}
func printSum(args []js.Value) {
c1 := args[0] // 结果显示到这个 div
numbers := args[1:] // 接下来是要累加的数字
c1.Set("innerHTML", sum(numbers))
}
func sum(numbers []js.Value) int {
var sum int
for _, val := range numbers {
sum += val.Int()
}
return sum
}
目前 Go 给 JavaScript 回调用的函数不支持返回值,未来也许会进一步支持,如果你想将结果带回 JavaScript 环境,就是以副作用的方式实现,例如改变某个 JavaScript 对象的状态,像是这边是改变某个 DOM 的innerHTML
。
因为 Go 的main
执行完,模块的程序就结束了,这样 Go 中定义的函数就没有了,然而,事件会是在之后才发生,因而要被回调的函数必须存活着,为了这个目的,范例中使用select {}
来阻断流程,视需求而定,你也可以用别的方式来设计某种阻断。
至于 JavaScript 的部份,来稍微修改一下 wasm_exec.html:
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</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();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
}).then(_ => { // 实例化模块之后就执行
console.clear();
go.run(inst);
});
</script>
<script>
function run() {
// 调用 Go 定义的回调函数
printSumTo(document.getElementById('c1'),
1, 2, 3, 4, 5);
}
</script>
<button onClick="run();" id="runButton" disabled>Run</button>
<div id="c1"></div>
</body>
</body>
</html>
按下 Run 之后,会调用runAndPrintSum
,这会先执行run
函数,执行 WebAssembly 模块实例,对应的就是执行 Go 定义的main
,因为run
是非同步的,接下来就会执行printSumTo
,因此 1 到 5 的累加结果,就会显示到id
为c1
的div
元素之中。
至于 WebAssembly API 的调整,想要了解这部份的话,可以看看〈WebAssembly〉中前三篇的说明。
故且不讨论 WebAssembly API 怎么写,在自定义的 JavaScript 代码中,想要调用 Go 中定义的函数,其实感觉就是多了些额外的手续,而且不自然。
如果把一切都带到 Go 中做,将 Go 中定义的函数,当成是某事件的回调,会比较单纯一些,例如:
package main
import (
"strconv"
"syscall/js"
)
func main() {
// 注册按钮事件
dom("runButton").Call("addEventListener", "click", js.NewCallback(cal))
select {}
}
// 根据 id 获取 DOM 对象
func dom(id string) js.Value {
return js.Global().Get("document").Call("getElementById", id)
}
// 按下 Run 的事件处理器
func cal(args []js.Value) {
n1, _ := inputValue("n1")
n2, _ := inputValue("n2")
dom("r").Set("innerHTML", n1+n2)
}
// 获取输入字段值
func inputValue(id string) (int, error) {
return strconv.Atoi(dom(id).Get("value").String())
}
至于 wasm_exec.html 可以如下调整:
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>
<body>
<input id="n1"> + <input id="n2"> = <span id="r"></span><br>
<button id="runButton" disabled>Run</button>
<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();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
}).then(_ => {
console.clear();
go.run(inst);
});
</script>
</body>
</html>
这样就可以进行页面操作,就是个简单的加法器:

(这也许才是 Go 希望的,要你把东西都带入 Go 中来做,JavaScript 环境的事件会调用 Go 的函数,然后在 Go 中计算,在 Go 中改变对象状态、画面等。)