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 }
Go wasm代碼

將代碼編譯成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。

如:

js.Global().Get("alert")
js.Global().Get("document")

如果是設置元素的屬性調用Set(),如果是呼叫(執行)方法,調用Call("函數名","參數")。

如:

js.Global().Get("document").Call("getElementById", id)
 
前面的代碼演示了調用alert()、Input的讀寫、Canvas對象的操作。

二、編寫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>
index.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 }
HTTP服務代碼

這里要注意:之前為了編譯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的基本套路了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM