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

這篇整理 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,最後卻在處理系統套件、編譯器版本、容器映像檔差異。

我自己最受不了的就是這種「可以用,但要先補一堆條件」的工具。你在本機跑得動,不代表 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」。這裡直接寫出來,反而省事。

我之前踩過一個坑:某個模組明明只是要跑一個小函式,卻因為 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 的背景則可參考 WebAssembly 和 WASI。我這篇有做整理、白話化跟實作模板化,其他都是基於原始資料延伸出來的。