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的基本套路了。