利用Golang玩转WebAssembly

Posted TSG前端技术分享

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用Golang玩转WebAssembly相关的知识,希望对你有一定的参考价值。

前言

WebAssembly,也叫WAMS,是一个可移植、体积小、加载快并且兼容 Web 的全新二进制格式,可以将C/C++/Rust等语言编写的代码编译为wasm文件然后运行在Web上。

用C/C++写wasm有点麻烦,需要安装各种编译器。话说最新Go 1.11发布了,其中有个新特性就是对WebAssembly的支持,今天我们就来玩玩,如何将Go代码编译为wasm文件。

提示:本文不会介绍Go的相关语法以及WASM的相关基础知识。如果阅读有困难,请先参考相关的基础介绍或阅读相关文章。

环境安装

环境安装就不多说了,直接参考官方安装文档就行,然后配置好对应的环境变量即可。例如在windows上,运行如下命令OK,就说明都正常了:

 
   
   
 
  1. C:\Users\Administrator>go version

  2. go version go1.11 windows/amd64

来个样例

先建立一个 main.go的文件,内容如下:

 
   
   
 
  1. package main

  2. func main() {

  3.    println("Hello, WebAssembly!")

  4. }

代码很简单,就是程序运行后直接在控制台输出 Hello,WebAssembly!

然后进入控制台(windows上需要先进入git bash),输入如下命令:

 
   
   
 
  1. GOARCH=wasm GOOS=js go build -o test.wasm main.go

这句话的意思是,切换Go的编译架构为wasm,运行的的对象为javascript引擎,然后源码为 main.go,最后输出为 test.wasm这个二进制文件

生成的 test.wasm需要加载到浏览器中才可以使用,为了方便使用,Go已经提供了默认的加载样例和脚本,可以通过把脚本和样例复制一下:

 
   
   
 
  1. cp $(go env GOROOT)/misc/wasm/wasm_exec.{html,js} .

上面的意思是把Go安装目录下的 wasm_exec.html和 wasm_exec.js复制到当前文件夹。一切就绪后,我们用 http-server(可以通过npm install http-server来安装)启动一个Web服务器:

 
   
   
 
  1. http-server -c 0 -p 2000

访问 http://localhost:2000/wasm_exec.html并打开控制台,会看到如下内容:

 
   
   
 
  1. Hello, WebAssembly!

深入样例

直接显示Hello world之类的提示不好玩,我们能否调用下Go里面的方法呢? 答案是可以的。我们首先修改下 wasm_exec.html,改成如下内容:

 
   
   
 
  1. <!doctype html>

  2. <html>

  3. <head>

  4.    <meta charset="utf-8">

  5.    <title>Go wasm</title>

  6. </head>

  7. <body>

  8.    <script src="wasm_exec.js"></script>

  9.    <script>

  10.        if (!WebAssembly.instantiateStreaming) { // polyfill

  11.            WebAssembly.instantiateStreaming = async (resp, importObject) => {

  12.                const source = await (await resp).arrayBuffer()

  13.                return await WebAssembly.instantiate(source, importObject)

  14.            }

  15.        }

  16.        const go = new Go()

  17.        let mod, inst

  18.        WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then( async result=> {

  19.            mod = result.module

  20.            inst = result.instance

  21.            await go.run(inst)

  22.        })

  23.        function wamsCallback(value) {

  24.            console.log(`wasm output: ${value}`)

  25.        }

  26.    </script>

  27.    <p>Hello</p>

  28.    <input id="name" />

  29. </body>

  30. </html>

接下来修改下 main.go,加入如下代码:

 
   
   
 
  1. package main

  2. import (

  3.    "syscall/js"

  4. )

  5. func calFib(n int) int {

  6.    a := 0

  7.    b := 1

  8.    for i := 0; i < n; i++ {

  9.        a, b = b, a+b

  10.    }

  11.    return b

  12. }

  13. func fib(params []js.Value) {

  14.    value := params[0].Int()

  15.    value = calFib(value)

  16.    // 当前Go和wasm交互,wasm没法直接获得函数的返回值,调用window.wamsCallback(value)或者直接window.output获取

  17.    // window.wamsCallback为用户在Javascript中自定义的函数,也就是一个回调函数

  18.    js.Global().Set("output", value)

  19.    js.Global().Call("wamsCallback", value)

  20. }

  21. // 将Go里面的方法注入到window.fibNative里面

  22. func registerCallbacks() {

  23.    js.Global().Set("fibNative", js.NewCallback(fib))

  24. }

  25. func main() {

  26.    registerCallbacks()

  27.    select {}

  28. }

上面的代码中,我们在Go中增加了一个计算斐波那契数列的方法,并在Go中通过 registerCallbacks这个方法注入到浏览器里面。 js.Global()表示获取宿主环境的window(浏览器)或者global(Node.js)。 Call表示调用对应的方法。所以wasm成功加载到浏览器中后,我们可以通过 window.fibNative这个函数来访问Go中的 fib方法。

Go现在对wasm的支持属于试验阶段,相关API还不完善,我们现在还没法直接获得返回值。在代码中,我们可以将返回值通过 js.Global().Set("output",value)将计算的返回值直接写到 window.output里面,或者调用我们页面中已有的函数。

在Chrome控制台,我们可以得到这样的结果:

 
   
   
 
  1. Hello, WebAssembly!

  2. > window.fibNative(3)

  3. wasm_exec.html:28 wasm output: 3

  4. > window.output

  5. 3

  6. > window.fibNative(30)

  7. wasm_exec.html:28 wasm output: 1346269

  8. > window.output

  9. 1346269

在Go中,通过 syscall/js这个官方提供的开发库还是可以调用页面中的DOM并操作,例如:

 
   
   
 
  1. package main

  2. import (

  3.    "syscall/js"

  4. )

  5. func changeBodyColor(color []js.Value) {

  6.    // document.body.style.color = color

  7.    js.Global().Get("document").Get("body").Set("style", "color:"+color[0].String())

  8. }

  9. func setInputValue(val []js.Value) {

  10.    id := val[0].String()

  11.    // document.getElementById(id).value = "value from Go"

  12.    js.Global().Get("document").Call("getElementById", id).Set("value", "value from Go")

  13. }

  14. // 将Go里面的方法注入到window.fibNative里面

  15. func registerCallbacks() {

  16.    js.Global().Set("changeBodyColor", js.NewCallback(changeBodyColor))

  17.    js.Global().Set("setInputValue", js.NewCallback(setInputValue))

  18. }

  19. func main() {

  20.    registerCallbacks()

  21.    select {}

  22. }

重新编译为wasm后,可以通过 window.changeBodyColor("red") 和 setInputValue("name")来切换页面颜色以及给文本框一个默认值。

再来一个计算数组和的样例:

 
   
   
 
  1. package main

  2. import (

  3.    "syscall/js"

  4. )

  5. func sum(params []js.Value) {

  6.    result := 0

  7.    for _, value := range params {

  8.        result += value.Int()

  9.    }

  10.    js.Global().Call("wamsCallback", result)

  11. }

  12. func registerCallbacks() {

  13.    js.Global().Set("sumNative", js.NewCallback(sum))

  14. }

  15. func main() {

  16.    println("Hello, WebAssembly!")

  17.    registerCallbacks()

  18.    select {}

  19. }

这样,我们可以通过 window.sumNative(1,2,3)来调用Go的计算和的方法

总结

以上是在Go中使用wasm的基本方法。当前Go对wasm的支持还属于实验阶段,官方也提到现在提供的API也不多,重要用于测试和验证。还有就是,当前的wasm二进制文件大小大约为1.2Mb左右,比于C生成的wasm要大不少,主要是里面包含的Go的一些运行时,所以会大点。以后也许会优化。

参考资料

  • Go中的WebAssembly

  • Go WebAssembly Tutorial - Building a Calculator

  • golang-wasm-example


以上是关于利用Golang玩转WebAssembly的主要内容,如果未能解决你的问题,请参考以下文章

Golang之WebAssembly篇

Rust + Go 双剑合璧:WebAssembly 领域应用

Go 语言宣布加入 WASM!WebAssembly 再添猛将

Gopher 的 WebAssembly 实践

golang goroutine例子[golang并发代码片段]

荧客技荐Go 语言宣布加入 WASM!WebAssembly 再添猛将