Go 中的 WASM 很棒:全網最全示例教程
WASM 的概念,這幾年還是挺火的,新的語言,比如 Rust、Go、Swift 等,都對 WASM 提供支持。相比之下,Go 語言的簡單性,使得對 WASM 的支持,使用起來也較簡單。本文是目前公開資料中爲數不多較完整的教程,希望能對你有幫助。
01 WASM 是什麼
標題說:“Golang 中的 Wasm 太棒了。”,但請用幾句話來說 “Wasm” 是什麼?
WebAssembly 主頁說:“WebAssembly(縮寫爲 Wasm)是一種基於堆棧的虛擬機的二進制指令格式。Wasm 被設計爲編程語言的可移植編譯目標,支持在 Web 上部署客戶端和服務器應用程序。”
總結就是:
- “Wasm 是一種可移植的格式(如 Java 或 .Net),你可以在任何有支持它的主機的地方執行它。最初,主要的主機是帶有瀏覽器的 JavaScript”。
現在,你可以用 JavaScript 和 NodeJS 運行 Wasm,我們最近看到了像 Wasmer 項目這樣的 Wasm 運行時的誕生,允許在任何地方運行 Wasm。
我喜歡說 “一個 wasm 文件就像一個容器鏡像,但更小,沒有操作系統”。
02 Wasm 是多語言的,但是...
你可以用多種語言編譯一個 Wasm 文件:C/C++、Rust、Golang、Swift …… 我們甚至看到了專門用於構建 Wasm 的語言的出現,比如 AssemblyScript[1] 或有前途的 Grain[2](可以密切關注它,語法很可愛)。
今年夏天,我決定開始使用 Wasm。這種趨勢似乎是使用 Rust,但我很快就明白我的小步驟會很複雜。困難不一定來自語言本身。最乏味和困難的部分是我在瀏覽器中運行一個簡單的 “Hello World” 所需的所有工具。經過一番搜索,我發現 Golang 爲 Wasm 提供了非常簡單的支持(比 Rust 簡單得多)。所以,我的假期作業是用 Golang 完成的。
Golang 對 Wasm 的支持非常棒。通常,WebAssembly 有四種數據類型(32 和 64 位整數,32 和 64 位浮點數),使用帶有字符串參數(甚至 JSON 對象)的函數可能會很混亂。幸運的是,Go 提供了wasm_exec.js
與 JavaScript API 交互的文件。
03 先決條件
要運行此博客文章的示例,你需要:
-
Golang 1.16
-
TinyGo 0.19.0(注意:TinyGo 0.19.0 不適用於 GoLang 1.17)
-
爲你的網頁提供服務的 http 服務器
順便說一句,爲了提供我的頁面,我使用帶有以下代碼的 Fastify[3] 項目:
index.js
const fastify = require('fastify')({ logger: true })
const path = require('path')
// Serve the static assets
fastify.register(require('fastify-static'), {
root: path.join(__dirname, ''),
prefix: '/'
})
const start = async () => {
try {
await fastify.listen(8080, "0.0.0.0")
fastify.log.info(`server listening on ${fastify.server.address().port}`)
} catch (error) {
fastify.log.error(error)
}
}
start()
我使用這個package.json
文件來安裝 Fastify(使用npm install
):
package.json
{
"dependencies": {
"fastify": "^3.6.0",
"fastify-static": "^3.2.1"
}
}
我在這裏創建了一個項目 https://gitlab.com/k33g_org/suborbital-demo,如果你用 GitPod[4] 打開它,你會得到一個準備好的開發環境,你不需要安裝任何東西。
04 必不可少的 “Hello World!”
創建一個項目
首先,創建一個hello-world
目錄,然後在該目錄中創建 2 個文件:
-
main.go
-
index.html
使用以下源代碼:
main.go
package main
import (
"fmt"
)
func main() {
fmt.Println("👋 Hello World 🌍")
// Prevent the function from returning, which is required in a wasm module
<-make(chan bool)
}
index.html
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
</head>
<body>
<h1>WASM Experiments</h1>
<script>
// This is a polyfill for FireFox and Safari
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer()
return await WebAssembly.instantiate(source, importObject)
}
}
// Promise to load the wasm file
function loadWasm(path) {
const go = new Go()
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
// Load the wasm file
loadWasm("main.wasm").then(wasm => {
console.log("main.wasm is loaded 👋")
}).catch(error => {
console.log("ouch", error)
})
</script>
</body>
</html>
備註:最重要的部分是:
這行代碼
<script src="wasm_exec.js"></script>
和這一行
WebAssembly.instantiateStreaming
,它是允許加載 wasm 文件的 JavaScript API。
你還需要go.mod
文件,使用以下命令生成一個:go mod init hello-world
,它的內容如下:
module hello-world
go 1.16
構建你的第一個 Wasm 模塊
在構建 Wasm 模塊之前,你需要獲取wasm_exec.js
文件,然後才能啓動編譯:
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
$ GOOS=js GOARCH=wasm go build -o main.wasm
現在,使用命令 node index.js
爲你的 html 頁面提供服務,以運行 Fastify http 服務器,並使用你喜歡的瀏覽器打開 http://localhost:8080
,然後打開開發人員控制檯工具:
wasm experiments
所以,上手很簡單,但是如果你查看 main.wasm
的大小,你會發現生成的文件大小在 2.1M 左右!!!老實說,我覺得這是不可接受的。幸運的是,使用 TinyGo 是一個較友好的解決方案。讓我們看看。
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/01-hello-world
05 TinyGo 版的 “Hello World”
首先,什麼是 TinyGo?TinyGo 允許爲微控制器編譯 Golang 源代碼,它也可以將 Go 代碼編譯爲 Wasm。TinyGo 是一個用於 “小地方” 的編譯器,因此生成的文件要小得多。
將你的 hello-world
項目複製到一個新目錄 hello-world-tinygo
並更改go.mod
文件的內容:
module hello-world-tinygo
go 1.16
在構建 Wasm 文件之前,這一次,你需要獲取 wasm_exec.js
與 TinyGo 的相關信息,然後才能編譯:
$ wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.19.0/targets/wasm_exec.js
$ tinygo build -o main.wasm -target wasm ./main.go
如果你提供 html 頁面,你將獲得與前一個示例相同的結果。但是看看 main.wasm
的大小。現在大小是 223K,小好多。
請記住,TinyGo 支持 Go 語言的一個子集,因此並非所有語言都可用(https://tinygo.org/docs/reference/lang-support/)。對於我的實驗來說,這就足夠了。
源代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/02-hello-world-tinygo
06 我的私藏貨
我看過太多冗長的教程,最終停留在這個簡單的 “hello world” 上而沒有進一步。他們甚至沒有解釋如何將參數傳遞給函數。通常,這只是項目 “入門” 的掩飾,沒有後續深入的講解。
今天,我給你分享所有讓你對 WASM 理解更多的私藏貨。
以下是我今天將介紹的 Wasm 和瀏覽器之間的不同交互:
-
與 DOM 交互
-
以字符串爲參數調用 Golang 函數來獲取字符串
-
如何通過 JavaScript 返回 “可讀” 的對象?
-
如何使用 JSON 對象作爲參數?
-
如何使用數組作爲參數?
-
如何返回一個數組?
與 DOM 交互
我們將使用"syscall/js"
這個 Go 包從 Go 代碼向 Html 文檔對象模型添加子標籤。根據文檔:“使用 js/wasm 架構時,Package js 可以訪問 WebAssembly 主機環境。它的 API 基於 JavaScript 語義。” . 這個包公開了一小組功能:類型 Value
(Go JavaScript 數據表示)和從 JavaScript 主機請求 Go 的方式。
-
通過複製前一個目錄創建一個新目錄並命名爲
dom
-
更新
go.mod
文件:module dom go 1.16
只需更改以下代碼 main.go
:
package main
import (
"syscall/js"
)
func main() {
message := "👋 Hello World 🌍"
document := js.Global().Get("document")
h2 := document.Call("createElement", "h2")
h2.Set("innerHTML", message)
document.Get("body").Call("appendChild", h2)
<-make(chan bool)
}
編譯代碼:tinygo build -o main.wasm -target wasm ./main.go
並提供 html 頁面 node index.js
,然後打開 http://localhost:8080/
。
-
我們得到了對 DOM 的引用
js.Global().Get("document")
-
我們創建了
<h2></h2>
元素document.Call("createElement", "h2")
-
我們通過
h2.Set("innerHTML", message)
來設置innerHTML
的值 -
最後將元素添加到 body 中
document.Get("body").Call("appendChild", h2)
完整源碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/03-dom
現在,讓我們看看如何創建一個可調用的 Go 函數,我們將在我們的 html 頁面中使用它。
調用 Go 函數
這一次,我們需要將函數 “導出” 到全局上下文中(即瀏覽器中的 window
,或 NodeJS 中的 global
)。"syscall/js"
這個 Go 包再次提供了必要的幫助程序來做到這一點。
像往常一樣,創建一個新目錄 first-function
(使用前面的示例)並通過 go.mod
更改模塊的值來更新文件:module first-function
。
這是的源代碼main.go
:
package main
import (
"syscall/js"
)
func Hello(this js.Value, args []js.Value) interface{} {
message := args[0].String() // get the parameters
return "Hello " + message
}
func main() {
js.Global().Set("Hello", js.FuncOf(Hello))
<-make(chan bool)
}
-
爲了導出函數到全局上下文,我們使用的
FuncOf
函數:js.Global().Set("Hello", js.FuncOf(Hello))
。該FuncOf
函數用於創建Func
類型。 -
該
Hello
函數接受兩個參數並返回一個interface{}
類型。該函數將從 Javascript 同步調用。第一個參數 (this
) 指的是 JavaScript 的global
對象。第二個參數是[]js.Value
表示傳遞給 Javascript 函數調用的參數的切片。
我們需要修改 index.html
文件來調用 Go 函數 Hello
:
index.html
:
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
</head>
<body>
<h1>WASM Experiments</h1>
<script>
// polyfill
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer()
return await WebAssembly.instantiate(source, importObject)
}
}
function loadWasm(path) {
const go = new Go()
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
loadWasm("main.wasm").then(wasm => {
console.log("main.wasm is loaded 👋")
console.log(Hello("Bob Morane"))
document.querySelector("h1").innerHTML = Hello("Bob Morane")
}).catch(error => {
console.log("ouch", error)
})
</script>
</body>
</html>
發生了什麼變化?,只有這兩行:
-
console.log(Hello("Bob Morane"))
:使用"Bob Morane"
作爲參數調用 Go 函數Hello
並在瀏覽器控制檯中顯示結果。 -
document.querySelector("h1").innerHTML = Hello("Bob Morane")
:使用"Bob Morane"
作爲參數調用 Go 函數Hello
並使用結果更改h1
的值。
所以,
-
構建 Wasm 文件:
tinygo build -o main.wasm -target wasm ./main.go
-
提供 html 頁面:
node index.js
你可以看到頁面內容已更新,但我們在控制檯中有一些錯誤消息。不用擔心,它很容易修復;這是一個已知錯誤 https://github.com/tinygo-org/tinygo/issues/1140,解決方法很簡單:
function loadWasm(path) {
const go = new Go()
//remove the message: syscall/js.finalizeRef not implemented
go.importObject.env["syscall/js.finalizeRef"] = () => {}
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
- 我只添加了這一行
go.importObject.env["syscall/js.finalizeRef"] = () => {}
以避免錯誤消息。
刷新頁面,沒有問題了。
現在,你幾乎擁有深入 WASM 所需的一切。不過,我想把我的更多私藏貨分享給你。
完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/04-first-function
07 我的其他私藏貨
如何通過 JavaScript 返回 “可讀” 的對象?
這一次,我們將 2 個字符串參數傳遞給 Hello
函數 ( firstName
和 lastName
),並使用類型 map[string]interface{}
返回一個 json 對象 :
Golang 函數:
func Hello(this js.Value, args []js.Value) interface{} {
firstName := args[0].String()
lastName := args[1].String()
return map[string]interface{}{
"message": "👋 Hello " + firstName + " " + lastName,
"author": "@k33g_org",
}
}
從 JavaScript 調用 Hello 函數很簡單:
loadWasm("main.wasm").then(wasm => {
let jsonData = Hello("Bob", "Morane")
console.log(jsonData)
document.querySelector("h1").innerHTML = JSON.stringify(jsonData)
}).catch(error => {
console.log("ouch", error)
})
爲你的頁面提供服務 node index.js
:
完整源碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/05-return-object
調用 Hello 時如何使用 Json 對象作爲參數?
如果我想在 JavaScript 中使用 Json 對象作爲參數,就像這樣:
let jsonData = Hello({firstName: "Bob", lastName: "Morane"})
我會像這樣寫我的 Golang 函數:
func Hello(this js.Value, args []js.Value) interface{} {
// get an object
human := args[0]
// get members of an object
firstName := human.Get("firstName")
lastName := human.Get("lastName")
return map[string]interface{}{
"message": "👋 Hello " + firstName.String() + " " + lastName.String(),
"author": "@k33g_org",
}
}
-
args[0]
包含 Json 對象 -
使用
Get(field_name)
方法檢索字段的值
完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/06-json-as-parameter
調用 Hello 時如何使用數組作爲參數?
JavaScript 調用:
let jsonData = Hello(["Bob", "Morane", 42, 1.80])
Golang 函數:
func Hello(this js.Value, args []js.Value) interface{} {
// get members of an array
firstName := args[0].Index(0)
lastName := args[0].Index(1)
age := args[0].Index(2)
size := args[0].Index(3)
return map[string]interface{}{
"message": "👋 Hello",
"firstName": firstName.String(),
"lastName": lastName.String(),
"age": age.Int(),
"size": size.Float(),
"author": "@k33g_org",
}
}
完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/07-array-as-parameter
如何返回一個數組?
Golang 函數:
func GiveMeNumbers(_ js.Value, args []js.Value) interface{} {
return []interface{} {1, 2, 3, 4, 5}
}
完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/08-return-an-array
就這些了吧。目前,我仍在學習 Wasm 和 Golang 的 Js 包,但我已經從中獲得了一些樂趣。希望你也一樣。
原文鏈接:https://blog.suborbital.dev/foundations-wasm-in-golang-is-fantastic
參考資料
[1]
AssemblyScript: https://www.assemblyscript.org/
[2]
Grain: https://grain-lang.org/
[3]
Fastify: https://www.fastify.io/
[4]
GitPod: https://www.gitpod.io/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HNaH775qMLqY0-Oq7BQEsQ