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 的主要作用和功能如下:
-
將 C/C++ 源代碼編譯成 WebAssembly 二進制格式 (.wasm 文件)
-
生成 JavaScript 源代碼用來加載和支持 WebAssembly 模塊
-
爲 C/C++ 代碼連接必要的 JavaScript 運行時支持 (如文件 I/O、多線程等)
-
將 C/C++ 標準庫封裝成 JavaScript 接口方便調用
-
支持 C++ 標準特性 (如 RTTI、異常等) 的編譯
-
優化編譯配置以減小文件體積
-
嵌入編譯進一步混淆代碼以提升性能
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
目錄下,包含了一些與特定平臺或用途相關的雜項文件。以下是其中的各個目錄和文件的作用:
- cgo/gmp:
-
fib.go: 包含一個使用 GMP 庫計算斐波那契數列的示例程序。
-
gmp.go: 提供對 GMP(GNU Multiple Precision Arithmetic Library)庫的 Go 綁定。
-
pi.go: 包含一個使用 GMP 庫計算圓周率的示例程序。
- chrome/gophertool:
-
README.txt: 有關 Chrome 擴展的說明文檔。
-
background.html: Chrome 擴展的後臺頁面 HTML。
-
background.js: Chrome 擴展的後臺頁面 JavaScript。
-
gopher.js: Chrome 擴展的 Gopher 圖標的 JavaScript 代碼。
-
gopher.png: Chrome 擴展中使用的 Gopher 圖標。
-
manifest.json: Chrome 擴展的清單文件。
-
popup.html: Chrome 擴展的彈出頁面 HTML。
-
popup.js: Chrome 擴展的彈出頁面 JavaScript。
- go_android_exec:
-
README: 有關在 Android 上執行 Go 程序的說明文檔。
-
exitcode_test.go: 包含與退出代碼相關的測試。
-
main.go: 包含一個在 Android 上執行的示例 Go 程序。
- ios:
-
README: 有關在 iOS 上執行 Go 程序的說明文檔。
-
clangwrap.sh: 提供用於 iOS 的 Clang 包裝腳本。
-
detect.go: 包含檢測 iOS 環境的 Go 代碼。
-
go_ios_exec.go: 包含在 iOS 上執行 Go 程序的 Go 代碼。
- linkcheck:
- linkcheck.go: 包含一個用於檢查鏈接的工具。
- wasm:
-
go_js_wasm_exec: 提供 Go 與 JavaScript 之間通信的支持。
-
go_wasip1_wasm_exec: 提供 Go 與 JavaScript 之間通信的支持(估計與 IP 地址相關)。
-
wasm_exec.html: 用於在瀏覽器中運行 WebAssembly 程序的 HTML 文件。
-
wasm_exec.js: WebAssembly 的 JavaScript 執行器。
-
wasm_exec_node.js: 用於在 Node.js 中運行 WebAssembly 程序的 JavaScript 文件。
這些文件和目錄主要包含了與 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.Value
和js.ValueOf
, 在 Go 的 WebAssembly(Wasm)和 JavaScript 交互中,js.Value
和 js.ValueOf
是兩個相關但不同的概念。
js.Value
:
-
js.Value
是 Go 語言中用於表示 JavaScript 值的類型。 -
它是一個接口,表示可以與 JavaScript 交互的值。
-
js.Value
接口提供了一系列方法,例如Get
、Set
、Call
,用於在 Go 中操作 JavaScript 對象和函數。
js.ValueOf
:
-
js.ValueOf
是一個函數,用於將 Go 中的基本類型或其他類型轉換爲js.Value
。 -
當需要將 Go 的值傳遞給 JavaScript 時,通常使用
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
}
在這個例子中:
-
使用
js.ValueOf
將 Go 的值轉換爲js.Value
。 -
使用
js.Global().Get("alert").Invoke
調用 JavaScript 的alert
函數。 -
使用
js.Global().Get("add").Call
調用 JavaScript 的自定義函數,並傳遞參數。 -
使用
js.FuncOf
創建一個 JavaScript 可調用的 Go 回調函數,然後通過js.Global().Set
註冊到全局對象。
新建 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>
-
<input>
元素的id="num"
用於輸入數字的輸入字段。 -
<button>
元素的id="btn"
具有一個onclick
屬性,其中包含 JavaScript 代碼。 -
onclick
屬性中的 JavaScript 代碼是ans.innerHTML=fibFunc(num.value * 1)
。它將具有id="ans"
的元素的innerHTML
設置爲調用名爲fibFunc
的函數的結果,該函數使用num
輸入字段中輸入的值。
這段 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)的簡單示例程序,它通過網頁上的按鈕觸發斐波那契數列的計算。
-
fib
函數定義了一個遞歸的斐波那契數列計算方法。 -
在
main
函數中,通過js.Global().Get("document")
獲取全局文檔對象,然後使用Call
方法獲取 HTML 文檔中的元素,包括輸入框 (num
)、段落 (ans
) 和按鈕 (btn
)。 -
fibFunc
函數是一個回調函數,它被註冊到按鈕的點擊事件上。當按鈕被點擊時,這個函數會讀取輸入框的值,將其轉換爲整數,然後調用斐波那契函數計算結果,並將結果更新到段落中。 -
在
main
函數中,通過js.FuncOf(fibFunc)
將 Go 函數轉換爲 JavaScript 函數,然後通過Call
方法將這個 JavaScript 函數註冊到按鈕的點擊事件上。 -
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 異步處理,並在等待期間更新網頁。
-
fib
函數定義了一個遞歸的斐波那契數列計算方法。 -
fibFunc
函數是一個回調函數,它被註冊到 JavaScript 中的fibFunc
函數。在計算斐波那契數列時,它通過 JavaScript 的回調方式異步執行,模擬了一個耗時的操作。在計算完成後,通過callback.Invoke(v)
將結果傳遞給 JavaScript 回調函數。 -
在
main
函數中,通過js.Global().Set("fibFunc", js.FuncOf(fibFunc))
將 Go 中的fibFunc
函數註冊到全局,以便 JavaScript 可以調用它。 -
time.Sleep(3 * time.Second)
模擬一個耗時的操作,延遲 3 秒。 -
在等待期間,通過
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