Wasm on Go

本篇內容,是對極客兔兔: Go WebAssembly (Wasm) 簡明教程 [1] 的實踐與記錄,主體內容來自這篇博客,推薦閱讀原文。

是否需要搭建 wasm 環境?

WebAssembly 上手 [2]

如果是 C/C++,需要藉助 emcc,將 C 和 C++ 代碼編譯到 WebAssembly 和 JavaScript。

在 Mac 上,

brew install emscripten

然後就可以使用  emcc 命令了

通過git clone https://github.com/emscripten-core/emsdk.git的方式編譯安裝, 可能有一堆坑 (可能和 Python 有關, 這個項目是用 Python 寫的,WebAssembly 開發環境搭建 - MAC[3], 直接繞道使用 brew)

emcc 是 Emscripten 的 C/C++ 到 WebAssembly 編譯器。

Emscripten 是一個項目, 它可以將 C 和 C++ 代碼編譯到 WebAssembly 和 JavaScript, 從而能在瀏覽器和 Node.js 中運行本來需要本地編譯的 C/C++ 代碼。

emcc 的主要作用和功能如下:

emcc 實際上是一個非常強大的交叉編譯器, 可以將大多數 C/C++ 代碼通過幾次編譯轉化成瀏覽器和 Node.js 可以理解和運行的 WebAssembly 與 JavaScript 組合。以實現在 web 環境中運行原本需要本地編譯的代碼。

但如果用 Go 或者 Rust, 就不需要這東西, 這些新語言原生支持 wasm

Go 對 wasm 的支持

Go 在 2018 年 8 月 24 號發佈的 1.11 版本 [4] 中, 增加了實驗性的 js/wasm, 算是對 Wasm 進行了原生的支持 (當然這個版本更重大的更新是 go module 這種依賴管理方式)。可以使用 go build 命令將 Go 程序編譯爲 WebAssembly 字節碼。

自那以後便可以說,Go 語言原生支持 WebAssembly 的編譯,可以將 Go 語言編寫的程序編譯成 wasm 格式,並在瀏覽器或其他支持 wasm 的環境中運行。

此外,Go 語言還提供了一些標準庫和工具,如 syscall/js 包和 wasm_exec.js 庫,用於與 JavaScript 交互和加載 WebAssembly 模塊。

而在此之前,如果想用 Go 開發前端,需用 GopherJS[5],這是一個可將 Go 轉換成能在瀏覽器中運行的 JavaScript 代碼的編譯器。
 
而 Go1.11 之後可以直接將 Go 代碼編譯爲 wasm 二進制文件,不再需要轉爲 JavaScript 代碼。(實現 GopherJS 和在 Go 語言中內建支持 WebAssembly 的是同一批人,包括後面會提到的 dmitshur[6] 大佬)

Go 語言實現的函數可以直接導出供 JavaScript 代碼調用,同時,Go 語言內置了 syscall/js 包,可以在 Go 語言中直接調用 JavaScript 函數,包括對 DOM 樹的操作

初入門徑 -- 使用 Go, 在網頁上彈出 Hello World

(1). 新建 main.go:

package main

import "syscall/js"

func main() {
 alert := js.Global().Get("alert")
 alert.Invoke("Hello World!")
}

IDE 一直飄紅, 這是因爲需要修改構建標記中的 OS 爲 js,Arch 爲 wasm

(2). 執行GOOS=js GOARCH=wasm go build -o static/main.wasm, 將 main.go 編譯爲 static/main.wasm (如果按上面設置了 GOOS 和 GOARCH,則可以直接go build -o static/main.wasm)

此時會新生成一個文件夾 static, 裏面有一個 main.wasm 文件

(3). 執行 cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static, 將 wasm_exec.js (JavaScript 支持文件,加載 wasm 文件時需要) 拷貝到 static 文件夾

misc 是 Go 源碼中的一個文件, 其目錄結構如下:

在 Go 語言源碼中的misc目錄下,包含了一些與特定平臺或用途相關的雜項文件。以下是其中的各個目錄和文件的作用:

  1. cgo/gmp:
  1. chrome/gophertool:
  1. go_android_exec:
  1. ios:
  1. linkcheck:
  1. wasm:

這些文件和目錄主要包含了與 Go 語言在不同平臺、環境下的一些特殊需求或功能相關的實用工具和示例。

(4). 在與 main.go 同級目錄下, 新建 index.html,引用 static/main.wasm 和 static/wasm_exec.js

<html>
<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>

</html>

(5). 使用 goexec 或者 npx http-server  啓動一個本地 Web 服務

其中 shurcooL/goexec[7] 是 Go 生態的, 是前面提到的社區中非常活躍的 Go 項目多次核心貢獻者 dmitshur 大佬寫的。 (GOTIME[8] 有采訪他的談話節目)

http-party/http-server[9] 則是一個簡單零配置的命令行 http 服務器, nodejs 開發.

此處使用後者, 執行 npx http-server

在瀏覽器中打開http://127.0.0.1:8080 (服務器默認使用 8080 端口,可以通過參數進行配置)

能看到如下彈框

另外可以看到請求的日誌

再上層樓 -- 註冊 (自定義) 函數(Register Functions)

上面是在 Go 中調用 js 的函數, 但 wasm 最大的價值之一, 是能在瀏覽器中執行一些對於 js 來說壓力太大的計算密集型操作.

在此用 Go 實現計算斐波那契數列的函數, 並註冊到 js 中, 可以讓其他 js 代碼調用

新建一個目錄, 創建一個 main.go 文件:

package main

import "syscall/js"

// fib 函數計算斐波那契數列的值
func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

// fibFunc 是一個JS回調函數,用於在JS中調用fib函數
func fibFunc(this js.Value, args []js.Value) interface{} {
 return js.ValueOf(fib(args[0].Int()))
}

func main() {
 done := make(chan int, 0)

 // 在全局對象上設置一個名爲 "fibFunc" 的JS函數,該函數調用fibFunc回調
 js.Global().Set("fibFunc", js.FuncOf(fibFunc))

 // 通過無限循環,使Wasm程序保持運行狀態;fibFunc 如果在 JavaScript 中被調用,會開啓一個新的子協程執行。
 <-done
}

以上這段程序演示如何在 WebAssembly 中使用 Go 語言編寫函數,並通過 JavaScript 調用這些函數。在這個例子中,fibFunc 函數充當了 Go 和 JavaScript 之間的橋樑,允許 JavaScript 代碼調用 Go 中定義的斐波那契數列計算函數。

關於js.Valuejs.ValueOf, 在 Go 的 WebAssembly(Wasm)和 JavaScript 交互中,js.Valuejs.ValueOf 是兩個相關但不同的概念。

  1. js.Value:
  1. js.ValueOf:

下面是一個簡單的例子,說明了它們的使用:

package main

import (
 "fmt"
 "syscall/js"
)

func main() {
 // 創建一個 js.Value 對象,表示 JavaScript 中的數字 42
 jsNumber := js.ValueOf(42)

 // 在 Go 中調用 JavaScript 的 alert 函數,並傳遞一個字符串
 js.Global().Get("alert").Invoke(js.ValueOf("Hello from Go!"))

 // 在 Go 中調用 JavaScript 函數,傳遞和獲取參數
 sum := js.Global().Get("add").Call(jsNumber, js.ValueOf(8))
 fmt.Println("Sum:", sum.Int())

 // 在 Go 中定義一個 JavaScript 回調函數,並傳遞給 JavaScript
 js.Global().Set("goCallback", js.FuncOf(goCallback))
 js.Global().Call("callJsFunction", js.Global().Get("goCallback"))

 // 保持程序運行,以便在瀏覽器中查看結果
 select {}
}

// goCallback 是一個在 JavaScript 中調用的 Go 回調函數
func goCallback(this js.Value, p []js.Value) interface{} {
 fmt.Println("Callback called from JavaScript!")
 return nil
}

在這個例子中:

新建 index.html:

<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>

</html>

相比於之前的頁面, 新增了一段塊, 增加一個輸入框, 按鈕, 文本框.   並給按鈕添加一個點擊事件, 將計算結果顯示在文本框中

執行GOOS=js GOARCH=wasm go build -o static/main.wasm, 將 main.go 編譯爲 static/main.wasm

執行 npx http-server

輸入任意數字, 能正確計算出結果

如果輸入的數字較大, 瀏覽器能直接把 CPU 跑滿..

牛刀再試 --- 操作 DOM

上面例子中 index.html 中 DOM 元素的操作, 是靠嵌入在 HTML 中的 JavaScript 代碼。

<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>

這段 JavaScript 代碼負責在按鈕點擊時更新具有 id="ans" 的段落(<p>)元素的內容。fibFunc 函數是一個斐波那契函數,接收來自 num 輸入字段的輸入值,計算斐波那契值,並在具有 id="ans" 的段落中顯示它。

希望能夠通過 Go 而不是 js 來操作 DOM 元素

新建一個項目, 名叫 dom,

新建 index.html, 去除操作 DOM 部分的 js 代碼:

<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>



</html>

新建 main.go:

package main

import (
 "strconv"
 "syscall/js"
)

func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

var (
 document = js.Global().Get("document")
 numEle   = document.Call("getElementById""num")
 ansEle   = document.Call("getElementById""ans")
 btnEle   = js.Global().Get("btn")
)

func fibFunc(this js.Value, args []js.Value) interface{} {
 v := numEle.Get("value")
 if num, err := strconv.Atoi(v.String()); err == nil {
  ansEle.Set("innerHTML", js.ValueOf(fib(num)))
 }
 return nil
}

func main() {
 done := make(chan int, 0)
 btnEle.Call("addEventListener""click", js.FuncOf(fibFunc))
 <-done
}

這是一個使用 Go 語言和 WebAssembly(Wasm)的簡單示例程序,它通過網頁上的按鈕觸發斐波那契數列的計算。

  1. fib 函數定義了一個遞歸的斐波那契數列計算方法。

  2. main 函數中,通過 js.Global().Get("document") 獲取全局文檔對象,然後使用 Call 方法獲取 HTML 文檔中的元素,包括輸入框 (num)、段落 (ans) 和按鈕 (btn)。

  3. fibFunc 函數是一個回調函數,它被註冊到按鈕的點擊事件上。當按鈕被點擊時,這個函數會讀取輸入框的值,將其轉換爲整數,然後調用斐波那契函數計算結果,並將結果更新到段落中。

  4. main 函數中,通過 js.FuncOf(fibFunc) 將 Go 函數轉換爲 JavaScript 函數,然後通過 Call 方法將這個 JavaScript 函數註冊到按鈕的點擊事件上。

  5. done := make(chan int, 0)<-done 是爲了保持程序運行,以便持續監聽事件。

該程序利用 Go 和 JavaScript 的互操作性,通過 WebAssembly 在瀏覽器中執行 Go 代碼,實現了一個簡單的交互式網頁。

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static

GOOS=js GOARCH=wasm go build -o static/main.wasm

npx http-server

紫禁之巔 --- 使用回調函數 (Callback Functions)

在 Js 中,異步+回調很常見,如請求一個 Restful API,註冊一個回調函數,待數據獲取到,再執行回調函數的邏輯. 這期間程序可以繼續做其他事。Go 語言可通過協程實現異步。
假設 fib 的計算非常耗時,那麼可以啓動註冊一個回調函數,待 fib 計算完成後,再把計算結果顯示出來。
先修改 main.go,使得 fibFunc 支持傳入回調函數。

新建一個目錄稱爲 callback

main.go:

修改 fibFunc, 使其支持傳入回調函數

package main

import (
 "syscall/js"
 "time"
)

func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

func fibFunc(this js.Value, args []js.Value) interface{} {
 callback := args[len(args)-1]
 go func() {
  time.Sleep(3 * time.Second)
  v := fib(args[0].Int())
  callback.Invoke(v)
 }()

 js.Global().Get("ans").Set("innerHTML""Waiting 3s...")
 return nil
}

func main() {
 done := make(chan int, 0)
 js.Global().Set("fibFunc", js.FuncOf(fibFunc))
 <-done
}

這是一個使用 Go 語言和 WebAssembly(Wasm)的示例程序,演示了在計算斐波那契數列時如何通過 Go 異步處理,並在等待期間更新網頁。

  1. fib 函數定義了一個遞歸的斐波那契數列計算方法。

  2. fibFunc 函數是一個回調函數,它被註冊到 JavaScript 中的 fibFunc 函數。在計算斐波那契數列時,它通過 JavaScript 的回調方式異步執行,模擬了一個耗時的操作。在計算完成後,通過 callback.Invoke(v) 將結果傳遞給 JavaScript 回調函數。

  3. main 函數中,通過 js.Global().Set("fibFunc", js.FuncOf(fibFunc)) 將 Go 中的 fibFunc 函數註冊到全局,以便 JavaScript 可以調用它。

  4. time.Sleep(3 * time.Second) 模擬一個耗時的操作,延遲 3 秒。

  5. 在等待期間,通過 js.Global().Get("ans").Set("innerHTML", "Waiting 3s...") 將網頁上顯示的信息更新爲 "Waiting 3s..."。

通過這個示例,展示瞭如何在 WebAssembly 中使用 Go 處理異步操作,並在等待時更新網頁內容。

  • 假設調用 fibFunc 時,回調函數作爲最後一個參數,那麼通過 args[len(args)-1] 便可獲取到該函數。這與其他類型參數的傳遞並無區別。

  • 使用 go func() 啓動子協程,調用 fib 計算結果,計算結束後,調用回調函數 callback,並將計算結果傳遞給回調函數,使用 time.Sleep() 模擬 3s 的耗時操作。

  • 計算結果出來前,先在界面上顯示 Waiting 3s...

新建 index.html,爲按鈕添加點擊事件,調用 fibFunc

<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="fibFunc(num.value * 1, (v)=> ans.innerHTML=v)">Click</button>
<p id="ans"></p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>



</html>
  • 爲 btn 註冊了點擊事件,第一個參數是待計算的數字,從 num 輸入框獲取。

  • 第二個參數是一個回調函數,將參數 v 顯示在 ans 文本框中。

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static

GOOS=js GOARCH=wasm go build -o static/main.wasm

npx http-server

會先顯示 Waiting 3s...,3s 過後顯示計算結果

更多推薦閱讀

go 編譯 wasm 與調用 [10]

Go 中的 WASM 很棒:全網最全示例教程 [11]

【Go】【WebAssembly】【wasm】基於 go 打包的網頁 wasm[12]

可能是世界上最簡單的用 Go 來寫 WebAssembly 的教程 [13]

如何在 Go 中使用 Wasm:淺聊 WebAssembly[14]

參考資料

[1]

極客兔兔: Go WebAssembly (Wasm) 簡明教程: https://geektutu.com/post/quick-go-wasm.html

[2]

WebAssembly 上手: https://www.cnblogs.com/Wayou/p/webassembly_quick_start.html

[3]

WebAssembly 開發環境搭建 - MAC: https://blog.csdn.net/daill894/article/details/103815099

[4]

1.11 版本: https://tip.golang.org/doc/go1.11

[5]

GopherJS: https://github.com/gopherjs/gopherjs

[6]

dmitshur: https://github.com/dmitshur

[7]

shurcooL/goexec: https://github.com/shurcooL/goexec

[8]

GOTIME: https://changelog.com/gotime

[9]

http-party/http-server: https://github.com/http-party/http-server

[10]

go 編譯 wasm 與調用: https://www.jianshu.com/p/69645c8bf57c

[11]

Go 中的 WASM 很棒:全網最全示例教程: https://www.qinglite.cn/doc/66216476ed6fe796d

[12]

【Go】【WebAssembly】【wasm】基於 go 打包的網頁 wasm: https://blog.csdn.net/sky529063865/article/details/126005525

[13]

可能是世界上最簡單的用 Go 來寫 WebAssembly 的教程: https://www.jiqizhixin.com/articles/2020-06-30-10

[14]

如何在 Go 中使用 Wasm:淺聊 WebAssembly: https://juejin.cn/post/7195217413091262523

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3qMzLd3h76j59fvAskZrrg