Go Wiki:WebAssembly

簡介

Go 1.11 加入 WebAssembly 的實驗性移植。Go 1.12 已改善部分內容,而 Go 1.13 預計會有進一步的改善。Go 1.21 加入新的移植目標,鎖定 WASI 系統呼叫 API。

首頁 中所述

WebAssembly(簡稱 Wasm)是一種為基於堆疊的虛擬機器設計的二進位指令格式。Wasm 是針對 C/C++/Rust 等高階語言進行編譯的可移植目標,讓客戶端和伺服器應用程式可以在網路部署。


如果您不瞭解 WebAssembly,請閱讀 入門,觀看一些 Go WebAssembly 的演講,然後查看下方的 其他範例


JavaScript (GOOS=js) 移植

入門

此頁面假設已安裝運作良好的 Go 1.11 或更新版本。如需疑難排解協助,請參閱 安裝疑難排解 頁面。

如果您使用 Windows,我們建議您使用 Git Bash 等 BASH 模擬器系統來遵循本教學。

對於 Go 1.23 及其之前的版本,本文所需要的 Webassembly 支援檔案位於 misc/wasm 中,在對檔案例如 lib/wasm/wasm_exec.js 執行操作時應該替換路徑。

為 Web 編譯基本 Go 套件

package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}

設定 GOOS=jsGOARCH=wasm 環境變數以針對 WebAssembly 編譯

$ GOOS=js GOARCH=wasm go build -o main.wasm

這會建置套件並產生名為 main.wasm 的可執行 WebAssembly 模組檔案。.wasm 檔案副檔名將使其更容易在稍後使用正確的 Content-Type 標頭透過 HTTP 提供服務。

請注意,你只能編譯主套件。否則,你會取得無法在 WebAssembly 中執行的物件檔案。如果你有想要與 WebAssembly 一起使用的套件,請將其轉換為主套件並建置二進位檔案。

要在瀏覽器中執行 main.wasm,我們還需要一個 JavaScript 支援檔案,以及一個 HTML 頁面來將所有內容串聯在一起。

複製 JavaScript 支援檔案

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

建立一個 index.html 檔案

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                go.run(result.instance);
            });
        </script>
    </head>
    <body></body>
</html>

如果你的瀏覽器尚不支援 WebAssembly.instantiateStreaming,你可以使用 polyfill

然後從網路伺服器提供這三個檔案 (index.htmlwasm_exec.js,和 main.wasm)。例如,使用 goexec

# install goexec: go install github.com/shurcooL/goexec@latest
goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'

或是使用你自己的 基本 HTTP 伺服指令

請注意:編譯器和 wasm_exec.js 支援檔案的 Go 版本必須相同。也就是說,如果 main.wasm 使用 Go 1.N 版本編譯,則對應的 wasm_exec.js 檔案也必須從 Go 1.N 版本複製。不支援其他組合。

請注意:要讓 goexec 指令在類 Unix 系統上運作,你必須 將 Go 的路徑環境變數 新增到殼層的 profile 中。Go 的入門指南有解釋如何執行這項操作

將 /usr/local/go/bin 新增到 PATH 環境變數。這可以透過將這一列新增到你的 /etc/profile (對於系統級安裝) 或 $HOME/.profile 中來達成

export PATH=$PATH:/usr/local/go/bin

請注意:對設定檔所做的變更可能要等到下次你登入電腦後才會套用

最後,導航至 https://127.0.0.1:8080/index.html,打開 JavaScript 除錯主控台,你應該就可以看到輸出了。你可以修改程式,重新建置 main.wasm,並重新整理以查看新的輸出。

使用 Node.js 執行 WebAssembly

相較於瀏覽器,也可用 Node.js 來執行已編譯的 WebAssembly 模組,這對於測試和自動化來說可能很有用。

首先,請確定已安裝 Node 且它在你的 PATH 中。

接着,將 $(go env GOROOT)/lib/wasm 加入你的 PATH 中。這樣就能讓 go rungo testPATH 搜尋中找到 go_js_wasm_exec 並使用它來支援 js/wasm

$ export PATH="$PATH:$(go env GOROOT)/lib/wasm"
$ GOOS=js GOARCH=wasm go run .
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test
PASS
ok      example.org/my/pkg  0.800s

如果你正在處理 Go 本身,這也能讓你順利執行 run.bash

go_js_wasm_exec 是一個封裝,讓你可以在 Node 中執行 Go Wasm 二進制檔案。預設情況下,它可以在你的 Go 安裝程式的 lib/wasm 目錄中找到。

如果你不想在 PATH 中加入任何東西,你也可以在你手動執行 go rungo test 時,將 -exec 標記設為 go_js_wasm_exec 的位址。

$ GOOS=js GOARCH=wasm go run -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" .
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec"
PASS
ok      example.org/my/pkg  0.800s

最後,這個封裝也可以用來直接執行 Go Wasm 二進制檔案

$ GOOS=js GOARCH=wasm go build -o mybin .
$ $(go env GOROOT)/lib/wasm/go_js_wasm_exec ./mybin
Hello, WebAssembly!
$ GOOS=js GOARCH=wasm go test -c
$ $(go env GOROOT)/lib/wasm/go_js_wasm_exec ./pkg.test
PASS
ok      example.org/my/pkg  0.800s

在瀏覽器中執行測試

你也可以使用 wasmbrowsertest,在你的瀏覽器中執行測試。它會自動執行啟動網路伺服器的工作,並使用無界面的 Chrome 在其中執行測試,再將記錄中繼到你的主控台。

和之前相同,只要執行 go get github.com/agnivade/wasmbrowsertest 就能取得一個二進制檔案。將其重新命名為 go_js_wasm_exec,並把它放到你的 PATH

$ mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec
$ export PATH="$PATH:$GOPATH/bin"
$ GOOS=js GOARCH=wasm go test
PASS
ok      example.org/my/pkg  0.800s

或者,使用 exec 測試標記。

GOOS=js GOARCH=wasm go test -exec="$GOPATH/bin/wasmbrowsertest"

與 DOM 互動

請參閱 https://pkg.go.dev/syscall/js

另外:

畫布

使用 net/http 時組態提取選項

你可以使用 net/http 函式庫從 Go 發出 HTTP 要求,它們會轉換成 提取 呼叫。然而,提取 選項 與 http 客戶端 選項之間沒有直接對應。為達成此目的,我們有一些特別標題值可以辨識為提取選項。這些值包括 -

因此,以範例來說,如果我們希望在發出要求時設定模式為「cors」,類似於

req, err := http.NewRequest("GET", "https://127.0.0.1:8080", nil)
req.Header.Add("js.fetch:mode", "cors")
if err != nil {
  fmt.Println(err)
  return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
  fmt.Println(err)
  return
}
defer resp.Body.Close()
// handle the response

請隨時訂閱 #26769 以取得更多背景資訊和更新資訊。

Chrome 中的 WebAssembly

如果你執行較新版的 Chrome,有一個標記 (chrome://flags/#enable-webassembly-baseline) 可以啟用 Liftoff,他們的新編譯器應該可以大幅縮短載入時間。更多資訊請見 這裡

更多範例

一般

畫布 (2D)

資料庫

WebGL 畫布(3D)

WASI (GOOS=wasip1) 移植

開始使用(WASI)

Go 1.21 加入 WASI 作為一個受支援的平台。要建立給 WASI,請使用 wasip1

$ GOOS=wasip1 GOARCH=wasm go build -o main.wasm

官方部落格有關於使用 WASI 埠的實用指南:https://go.dev.org.tw/blog/wasi

Go WebAssembly 演講

編輯器組態

除錯

WebAssembly 目前尚不支援任何除錯工具,因此您目前必須使用老式的 println() 方法在 JavaScript 主控台中顯示輸出。

為了解決這個問題,已成立官方的 WebAssembly 除錯小組,並陸續進行初步調查和提議

如果您有興趣參與除錯工作,請盡情參與並協助推動。:smile

分析 WebAssembly 檔案的結構

WebAssembly 程式碼瀏覽器 可用於將 WebAssembly 檔案的結構視覺化。

縮小 Wasm 檔案大小

目前,Go 會產生大型 Wasm 檔案,而最小的檔案大小約為 2MB。如果您的 Go 程式碼載入函式庫,此檔案大小可能會大幅增加。10MB 以上的情況很常見。

目前有兩種主要方式可以縮小此檔案大小

  1. 手動壓縮 .wasm 檔案。

    • 使用 gz 壓縮可將最小檔案大小 (~2MB) 的範例 WASM 檔案壓縮至約 500kB。您可能需要改用 Zopfli 進行 gzip 壓縮,因為它比 gzip --best 能提供更好的結果,但執行時間也更長。
    • 使用 Brotli 進行壓縮,檔案大小明顯比 Zopfli 和 gzip --best 更小,而壓縮時間也介於這兩者之間。這個 (新的) Brotli 壓縮器 看起來不錯。

    @johanbrandhorst 提供的範例

    範例 1

    大小 指令 壓縮時間
    16M (未壓縮大小) 不適用
    2.4M brotli -o test.wasm.br test.wasm 53.6 秒
    3.3M go-zopfli test.wasm 3 分鐘 2.6 秒
    3.4M gzip --best test.wasm 2.5 秒
    3.4M gzip test.wasm 0.8 秒

    範例 2

    大小 指令 壓縮時間
    2.3M (未壓縮大小) 不適用
    496K brotli -o main.wasm.br main.wasm 5.7 秒
    640K go-zopfli main.wasm 16.2 秒
    660K gzip --best main.wasm 0.2 秒
    668K gzip main.wasm 0.2 秒

    使用像 https://github.com/lpar/gzipped 這樣的東西,在有壓縮檔和正確的標頭時自動提供服務。

  2. 使用 TinyGo 來產生 Wasm 檔。

    TinyGo 支援針對嵌入式裝置的 Go 語言子集,並有 WebAssembly 輸出目標。

    雖然它有一些限制(還不是完整的 Go 實作),但它仍然相當有能力,且產生的 Wasm 檔案非常小。大約 10kB 並不罕見。「Hello world」範例是 575 位元組。如果你使用 gz -6 壓縮,它會降到 408 位元組。 :wink

    這個專案也積極開發中,因此它的功能正快速擴充。見 https://tinygo.org/docs/guides/webassembly/ 以取得更多有關使用 TinyGo 和 WebAssembly 的資訊。

其他 WebAssembly 資源


這個內容是 Go Wiki 的一部分。