Go进行wasm编程
wasm即webAssemble,是一种不针对特定平台的二进制格式文件。Go从1.11开始支持wasm,最初通过js.NewCallBack()注册函数,1.12开始换成了FuncOf()。
Go开发wasm需要一个go文件用于编写实现代码,编译成.wasm文件;需要一个wasm_exec.js文件,这个是Go提供的,可以从 Go 安装目录的 misc 子目录里找到,将它直接拷贝过来。它实现了和 WebAssembly 模块交互的功能;另外就是需要一个HTML文件用于加载wasm文件。当然为了工作起来,我们还要实现一个简单的HTTP服务。
一、用Go编写代码并编译成wasm文件
1 package main 2 3 import ( 4 "fmt" 5 "math/rand" 6 "strconv" 7 "syscall/js" 8 "time" 9 ) 10 11 const ( 12 width = 400 13 height = 400 14 ) 15 16 // 生成 0 - 1 的随机数 17 func getRandomNum() float32 { 18 rand.New(rand.NewSource(time.Now().UnixNano())) 19 n := float32(rand.Intn(10000)) 20 return n / 10000.0 21 } 22 23 // 生成 0 - 10 的随机数 24 func getRandomNum2() float32 { 25 rand.New(rand.NewSource(time.Now().UnixNano())) 26 n := float32(rand.Intn(10000)) 27 return n / 1000.0 28 } 29 30 // 使用 canvas 绘制随机图 31 func draw() { 32 var canvas js.Value = js. 33 Global(). 34 Get("document"). 35 Call("getElementById", "canvas") 36 37 var context js.Value = canvas.Call("getContext", "2d") 38 39 // reset 40 canvas.Set("height", height) 41 canvas.Set("width", width) 42 context.Call("clearRect", 0, 0, width, height) 43 44 // 随机绘制 50 条直线 45 var clineStyle = `rgba(%d, %d, %d, 0.5)` 46 for i := 0; i < 50; i++ { 47 lineStyle := fmt.Sprintf(clineStyle, 155+int(getRandomNum2()*10), 155+int(getRandomNum()*100), 155+int(getRandomNum()*100)) 48 fmt.Println(lineStyle) 49 context.Call("beginPath") 50 context.Set("strokeStyle", lineStyle) 51 context.Call("moveTo", getRandomNum()*width, getRandomNum()*height) 52 context.Call("lineTo", getRandomNum()*width, getRandomNum()*height) 53 context.Call("stroke") 54 } 55 56 context.Set("font", "30px Arial") 57 context.Set("strokeStyle", "blue") 58 for i := 0; i < 10; i++ { 59 context.Call("strokeText", "hello wasm", (getRandomNum2()+1)*10+getRandomNum2()*10, (getRandomNum2()+1)*10+getRandomNum2()*50) 60 } 61 } 62 63 func registerCallbackFunc() { 64 cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 65 fmt.Println("button clicked") 66 67 num1 := getElementByID("num1").Get("value").String() 68 v1, err := strconv.Atoi(num1) 69 if nil != err { 70 fmt.Println("button clicked:", num1, err.Error()) 71 jsAlert().Invoke(err.Error()) 72 // panic(err) 73 return nil 74 } 75 76 num2 := getElementByID("num2").Get("value").String() 77 v2, err := strconv.Atoi(num2) 78 if nil != err { 79 fmt.Println("button clicked:", num2, err.Error()) 80 // panic(err) 81 return nil 82 } 83 84 rlt := v1 + v2 85 getElementByID("rlt").Set("value", rlt) 86 87 return nil 88 }) 89 90 getElementByID("compute").Call("addEventListener", "click", cb) 91 } 92 93 func getElementByID(id string) js.Value { 94 return js.Global().Get("document").Call("getElementById", id) 95 } 96 97 func jsAlert() js.Value { 98 return js.Global().Get("alert") 99 } 100 101 func main() { 102 fmt.Println("Hello, Go WebAssembly!") 103 draw() 104 // 通过js.Global().Get()拿到全局alert函数的引用 105 alert := js.Global().Get("alert") 106 // 调用alert.Invoke来调用alert函数 107 alert.Invoke("hello world") 108 109 registerCallbackFunc() 110 }
将代码编译成Wasm文件,需要设置编译环境。我用的VsCode,用powershell设置环境变量始终不能生效,于是换成了Bash:
执行:go env 查看环境,注意GOOS和GOARCH,如果是win 系统的话,默认应该是windows和amd64,为了编译出wasm文件,需要修改如下:
export GOOS=js
export GOARCH=wasm
否则编译的时候会提示奇怪的信息(不是提示环境问题),如果还是不对,可以设置CGO:
export CGO_ENABLED=0
当然我设置的1是没问题的。
最后编译生成wasm文件:
go build -o lib.wasm main.go
-o 是编译参数,指定输出的文件。
在Go里面要引入:syscall/js
通过js.Global().Get()获取js对象,既可以获取函数、也可以获取DOM元素。类型是js.Value。
如:
如果是设置元素的属性调用Set(),如果是呼叫(执行)方法,调用Call("函数名","参数")。
如:
二、编写HTML
1 <html> 2 <head> 3 <meta charset="utf-8"> 4 <script src="wasm_exec.js"></script> 5 <script> 6 const go = new Go(); 7 WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then((result) => { 8 go.run(result.instance); 9 }); 10 </script> 11 </head> 12 <body> 13 <canvas id='canvas'></canvas></br> 14 <input id="num1" type="number" /> 15 + 16 <input id="num2" type="number" /> 17 = 18 <input id="rlt" type="number" readonly="readonly" /> 19 <button id="compute">compute</button> 20 </body> 21 </html>
HTML文件主要是定义界面元素,引入wasm_exec.js文件,调用刚才build的lib.wasm。
三、编写一个HTTP服务
Go 内置的 HTTP 服务器支持Content-Type 为 application/wasm。
1 package main 2 3 import ( 4 "flag" 5 "log" 6 "net/http" 7 ) 8 9 var ( 10 listen = flag.String("listen", ":8087", "listen address") 11 dir = flag.String("dir", ".", "files directory to serve") 12 ) 13 14 func main() { 15 flag.Parse() 16 log.Printf("listening on %q...", *listen) 17 err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))) 18 log.Fatalln(err) 19 }
这里要注意:之前为了编译wasm文件,修改了GOOS和GOARCH,现在为了运行http服务,我们必须恢复。
为了方便调试,我们可以在vscode里面新建一个终端,执行:
export GOOS=windows
export GOARCH=amd64
然后执行:
go run server.go
如果有防火墙提示网络访问,选择允许,然后会看到终端提示:
2020/03/10 09:27:12 listening on ":8087"...
这表示我们的HTTP服务启动好了。
四、测试效果
在浏览器里面输入:http://127.0.0.1:8087/
可以看到页面弹出了对话框:
然后出现了我们绘制的内容:
在浏览器调试器里面看到输出内容:
页面上还有一个计算的功能,我们输入数字,点击按钮,发现没有反应,看调试器可以看见错误:
信息提示很明确,回头看我们的Go代码,main()函数在执行了registerCallbackFunc()就结束退出了,
这个时候再去调用肯定是失败的,所以我们要让程序不能退出:
1 func main() { 2 fmt.Println("Hello, Go WebAssembly!") 3 draw() 4 // 通过js.Global().Get()拿到全局alert函数的引用 5 alert := js.Global().Get("alert") 6 // 调用alert.Invoke来调用alert函数 7 alert.Invoke("hello world") 8 done := make(chan struct{}, 0) // 创建无缓冲通道 9 10 registerCallbackFunc() 11 <-done // 阻塞 12 }
在第8行创建一个通道,然后在11行从通道读取内容,因为通道没有内容,所以会阻塞。
然后重新编译wasm文件,刷新网页,可以看到预期达到了:
这就是用Go开发Wasm的基本套路了。