[TOOLS] 11 分鐘閱讀OraCore 編輯部

Wazero 讓 Go Wasm 變回純 Go

我拆解 wazero 的 Go Wasm 選型邏輯,整理成純 Go、免 CGO、可直接套用的實作模板。

分享 LinkedIn
Wazero 讓 Go Wasm 變回純 Go

這篇整理 wazero 在 Go 專案裡怎麼用、什麼情境最適合、以及可以直接貼上的最小模板。

我用 Go 跑 WebAssembly 一陣子了。理論上很美,實際上很煩。你明明只是想在 Go 服務裡跑一個 wasm 模組,結果一下要顧 CGO,一下要顧交叉編譯,一下又冒出一堆「先把這個原生依賴裝好」的前置條件。Go 本來最討厭的就是這種黏黏的東西,偏偏很多 runtime 就愛把事情搞複雜。我後來才發現,我不是在找最強的 runtime,我是在找一個不要跟 Go 作對的 runtime。

這次我拆的是 wasmRuntime 的 Go WebAssembly runtime 指南,它的推薦很直接:Go 專案先看 wazero。不是因為它最花俏,而是因為它夠乾淨,乾淨到你不太需要跟 build system 打架。這種選型我很買單,因為我維護服務的時候,最怕的不是少一個功能,是多一個我根本不想處理的坑。

Wazero 的重點不是強,是不鬧事

訂閱 AI 趨勢週報

每週精選模型發布、工具應用與深度分析,直送信箱。不定期,不騷擾。

不會寄垃圾信,隨時可取消。

「Wazero is a zero-dependency WebAssembly runtime for Go applications.」

翻譯一下就是:它是給 Go 用的,而且盡量不要把你拖進額外依賴地獄。這句話看起來很普通,但對 Go 開發者來說超重要。Go 的爽感就是編譯、部署、交叉編譯都盡量一致;一旦 runtime 開始依賴 CGO,整個故事就會變味。你本來只想處理 wasm,最後卻在處理系統套件、編譯器版本、容器映像檔差異。

Wazero 讓 Go Wasm 變回純 Go

我自己最受不了的就是這種「可以用,但要先補一堆條件」的工具。你在本機跑得動,不代表 CI 跑得動;CI 跑得動,不代表 ARM 機器跑得動。wazero 的價值就在這裡,它把 runtime 層拉回 Go 自己的語言邏輯裡,不逼你另外學一套部署宗教。

實操寫法很簡單:如果你的需求是「在 Go 服務裡載入 wasm、呼叫 exported function、做 sandbox 或 plugin 執行」,我會先從 wazero 開始。不要先去追那些看起來很潮的 runtime,先確認你到底是不是只需要一個能穩穩跑的執行層。

  • Go-first 專案,先選純 Go runtime。
  • 要做嵌入式 wasm,先避開 CGO。
  • 如果目標是少維運成本,wazero 很合適。

純 Go 不是加分項,是門檻

「Pure Go implementation means no CGO, easy cross-compilation, and Go-native integration.」

這句我會直接翻成白話:你不用另外養一條原生工具鏈,能省掉一堆莫名其妙的部署問題。這不是什麼漂亮口號,這是實際維運時會救命的事。很多 runtime 的問題不是功能不夠,是它把你的 build 流程搞得像拼圖。今天少一個 library,明天少一個 header,後天容器裡還少一個系統套件。

我以前在一個團隊看過很典型的狀況:大家都說「只是加一個 wasm 執行能力」,結果最後 CI pipeline 多了幾個步驟,Docker image 也膨脹,發版時還要確認不同平台的原生依賴。那種感覺很像你只是想換燈泡,結果被迫重拉全屋電線。wazero 至少不會逼你先做這些事。

實操上,我會把「是否純 Go」當成第一道篩選條件。只要你的服務有多架構部署、靜態編譯、容器化發版,這個條件就不是偏好,是硬需求。你可以先不管誰 benchmark 比較漂亮,先問自己:這個 runtime 會不會讓我在 build 階段多養一個世界?如果答案是會,先放一邊。

  • 需要多平台部署,就先看純 Go。
  • 需要穩定 CI,就先避開 CGO。
  • 需要可預期的交叉編譯,就別讓 runtime 亂加戲。

它的 API 夠直白,才像 Go

package main

import (
    "context"
    "os"

    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)

    wasi_snapshot_preview1.MustInstantiate(ctx, r)
    wasm, _ := os.ReadFile("module.wasm")
    mod, _ := r.InstantiateModuleFromBinary(ctx, wasm)
    result, _ := mod.ExportedFunction("add").Call(ctx, 1, 2)
    println(result[0])
}

這段我很喜歡,因為它沒有把簡單事情寫成儀式。建立 runtime、載入 wasm、呼叫函式、收尾,整個流程都很像 Go 本來就該長的樣子。你不需要先學一堆抽象名詞,也不用在文件裡翻半天才知道「到底要不要先啟用 WASI」。這裡直接寫出來,反而省事。

Wazero 讓 Go Wasm 變回純 Go

我之前踩過一個坑:某個模組明明只是要跑一個小函式,卻因為 host capability 的設定不清楚,整個整合卡了半天。最後不是程式邏輯錯,而是我根本沒搞懂 runtime 預設幫我做了什麼。wazero 的好處是,它把這些東西攤在你眼前,讓你知道自己到底在接什麼。

實操寫法就是先縮小範圍。不要一開始就想把整個產品流程都塞進 wasm。先做一個模組、一個 exported function、必要時再加 WASI。你先把最小可行路徑跑通,後面要擴充才有底。

WASI 要用就明講,不要裝沒事

「WASI Preview 1 only. No Component Model yet.」

這句其實很誠實,也很重要。wazero 不是什麼都能吞的萬能解法,它比較像一把乾淨的刀,切得漂亮,但有邊界。你如果現在的需求是標準支援更廣、Component Model 已經是你路線圖上的重點,那你就不能只看「純 Go 很爽」這一點。

白話一點講,wazero 很適合「我想在 Go 裡執行 wasm」這件事,但不適合你把它當成「我要追最新 Wasm 規格全餐」的答案。這兩件事差很多。前者是務實需求,後者是架構野心。很多團隊的問題就是把野心當需求,最後選了不合適的 runtime,然後怪工具不夠好。

我會怎麼做?先列需求清單,而且只列今天真的要用的。你如果目前只是要載入模組、呼叫函式、處理少量 host 互動,那 wazero 很合理。你如果已經確定要碰 Component Model、要更完整的標準相容性,那就別硬撐,直接把 Wasmtime 拉進比較表。

  • 先確認你需要的是 WASI Preview 1 還是別的版本。
  • 先確認你的模組有沒有 Component Model 需求。
  • 先確認你要的是執行穩定,還是規格覆蓋更廣。

別拿 runtime 清單當戰利品牆

「Other options: Wasmtime, Wasmer, WasmEdge, Spin, GraalWasm, Wasm3, WAMR, Wasmi, Chicory, Lunatic.」

我很討厭一種比較方式:把一堆 runtime 排成表,然後假裝只要欄位多一點就代表更好。事實不是這樣。runtime 不是蒐集品,你不需要因為有很多選項就覺得自己選到了最厲害的。你要看的只有一件事:它是不是符合你現在的 host 語言、部署方式、以及你能接受的維運成本。

這份指南最有用的地方,就是它沒有裝中立。它其實很明確地說,對 Go 來說,wazero 是最順的起點。這種講法我反而比較信,因為它不是在賣夢,而是在講工作流。Go 開發者最怕的不是少功能,而是多一層沒必要的複雜度。

實操寫法我會建議這樣做:先用你的最強約束來選。你的最強約束如果是「純 Go」,那就是 wazero。你的最強約束如果是「規格支援更完整」,那就看 Wasmtime。你的最強約束如果是別的,例如 AI 推論或特殊嵌入場景,再去看其他候選。不要反過來,先把所有候選看完再頭痛。

我會怎麼落地這個決定

如果只是要我給一句結論,我會說:Go 專案裡要跑 wasm,先用 wazero。原因不是它最炫,而是它最像 Go。你不用為了 runtime 改整個 build 流程,也不用把部署故事拆成兩套。這種「少一個維運問題」的價值,通常比多一個漂亮功能更實際。

但我也不會把它吹成萬靈丹。你只要需求一碰到 Component Model、更新的 Wasm 規格、或你很在意某些進階標準支援,就要重新評估。這不是退讓,這是正常選型。工具本來就該為需求服務,不是反過來。

我自己的判斷順序很固定:先看是不是純 Go,再看是不是要 CGO,再看 wasm 特性覆蓋範圍,最後才看文件風格跟社群熱度。因為前面三個如果不對,後面再熱鬧都沒用。你終究還是得把它塞進 CI、塞進容器、塞進真實服務裡跑。

可抄的模板

# Go + wazero 最小可用模板

## 什麼時候用
我會在這種情況用它:
- 需要在 Go 服務裡執行 WebAssembly
- 想避免 CGO
- 想保留交叉編譯與靜態部署的簡潔性
- 先從最小功能驗證,不想一開始就把架構做大

## 安裝
bash
go get github.com/tetratelabs/wazero


## 最小程式
go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()

    runtime := wazero.NewRuntime(ctx)
    defer runtime.Close(ctx)

    // 只有模組真的需要 WASI 才開。
    wasi_snapshot_preview1.MustInstantiate(ctx, runtime)

    wasmBytes, err := os.ReadFile("module.wasm")
    if err != nil {
        panic(err)
    }

    mod, err := runtime.InstantiateModuleFromBinary(ctx, wasmBytes)
    if err != nil {
        panic(err)
    }

    fn := mod.ExportedFunction("add")
    if fn == nil {
        panic("找不到 exported function: add")
    }

    results, err := fn.Call(ctx, 1, 2)
    if err != nil {
        panic(err)
    }

    fmt.Println(results[0])
}


## 我自己的檢查清單
- 先確認模組真的需要哪個 WASI 版本
- 先只接一個 exported function
- 先只跑一條執行路徑,不要一次接整個產品流程
- 先確認 CI、容器、交叉編譯都能過
- 只要沒有硬需求,就先維持純 Go,不要碰 CGO

## 選型規則
選 wazero,如果:
- 你是 Go-first 專案
- 你在意部署簡單
- 你在意交叉編譯
- 你暫時不需要 Component Model

改看 Wasmtime,如果:
- 你需要更完整的標準支援
- 你已經確定要碰 Component Model
- 你能接受 CGO 或較重的建置鏈

## 最後一步
先把這段接到一個內部工具、單一插件入口,或一條 sandbox 執行路徑上,確認真的有價值,再決定要不要擴大。

這個模板我刻意寫得很小,因為我真的不想再看那種一上來就包三層抽象、結果沒人敢改的範例。能跑、能懂、能複製,這三件事比「看起來很完整」重要太多。你先把它塞進 repo,真的跑過一次,再來談要不要擴充。

來源我主要拆的是 wasmRuntime 的 Go WebAssembly runtime 指南,原始推薦邏輯與比較框架來自那篇內容;wazero 相關資訊可看 wazero GitHub,替代方案可看 Wasmtime GitHub,Wasm 與 WASI 的背景則可參考 WebAssemblyWASI。我這篇有做整理、白話化跟實作模板化,其他都是基於原始資料延伸出來的。