あなたが書いた、あのパフォーマンスの高いGoのロジックを、サーバーなしで、直接ユーザーのブラウザ上で動かせるとしたら…?これはもはや未来の話ではありません。WebAssembly(Wasm)が、その夢を現実にします。
この記事では、Wasmの基本からGoプログラムをWasmにコンパイルし、ブラウザ上で動かすまでの「Hello, World」を、実践的なチュートリアル形式で解説します。
WebAssemblyはブラウザで実行可能なバイナリ形式のコードです。高速、安全、ポータブルという特徴を持ちます。
Goで使うメリット:
実際に手を動かしながら、WebAssemblyの世界を体験してみましょう。
main.go
)ブラウザのコンソールにメッセージを出力する、シンプルなプログラムから始めます。
go1package main 2 3import ( 4 "log" 5 "syscall/js" 6) 7 8func main() { 9 // ブラウザのコンソールに出力 10 log.Println("Hello, WebAssembly from Go!") 11 12 // JavaScriptのconsole.logを直接呼び出す方法 13 js.Global().Get("console").Call("log", "Hello from Go via console.log!") 14}
GOOS=js
とGOARCH=wasm
環境変数を使い、.wasm
ファイルにコンパイルします。
bash1# 基本的なコンパイル 2GOOS=js GOARCH=wasm go build -o main.wasm main.go 3 4# バイナリサイズ最適化版(推奨) 5GOOS=js GOARCH=wasm go build -ldflags="-s -w" -trimpath -o main_raw.wasm main.go 6 7# さらなる最適化(wasm-optが利用可能な場合) 8# macOS / Linux (Homebrew) 9brew install binaryen # wasm-opt が含まれる 10 11# Windows (Scoop) 12scoop install wasm-opt 13 14# 最適化実行 15wasm-opt -Oz main_raw.wasm -o main.wasm
最適化フラグの説明:
-ldflags="-s -w"
: デバッグ情報とシンボルテーブルを削除-trimpath
: ビルドパスを除去してサイズ削減wasm-opt -Oz
: Binaryenツールによる高度な最適化wasm_exec.js
)GoのWasmを実行するためには、「接着剤」の役割を果たすwasm_exec.js
が必要です。
bash1# Go 1.22以降では場所が変更されている場合があります 2# 将来的にはlib/wasmに一本化される予定 3if [ -f "$(go env GOROOT)/lib/wasm/wasm_exec.js" ]; then 4 cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" . 5elif [ -f "$(go env GOROOT)/misc/wasm/wasm_exec.js" ]; then 6 cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . 7else 8 echo "wasm_exec.js not found" 9fi
本番環境ではgo:embed
を使ってWasmファイルを組み込むことで、単一バイナリでの配布が可能です。
go1// server_embed.go 2package main 3 4import ( 5 _ "embed" 6 "log" 7 "net/http" 8) 9 10//go:embed main.wasm 11var wasmBytes []byte 12 13//go:embed wasm_exec.js 14var wasmExecJS []byte 15 16func main() { 17 // 組み込まれたWasmファイルを配信 18 http.HandleFunc("/main.wasm", func(w http.ResponseWriter, r *http.Request) { 19 w.Header().Set("Content-Type", "application/wasm") 20 w.Write(wasmBytes) 21 }) 22 23 // 組み込まれたwasm_exec.jsを配信 24 http.HandleFunc("/wasm_exec.js", func(w http.ResponseWriter, r *http.Request) { 25 w.Header().Set("Content-Type", "application/javascript") 26 w.Write(wasmExecJS) 27 }) 28 29 log.Println("Server starting on :8080") 30 log.Fatal(http.ListenAndServe(":8080", nil)) 31}
次に、Wasmを読み込むindex.html
を作成します。
html1<!DOCTYPE html> 2<html lang="ja"> 3<head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width,initial-scale=1"> 6 <title>Go WebAssembly Example</title> 7</head> 8<body> 9 <!-- defer属性でパフォーマンス最適化(推奨) --> 10 <script src="wasm_exec.js" defer></script> 11 <script type="module" defer> 12 const go = new Go(); 13 // Safari対応のフォールバック付きWasm読み込み 14 async function loadWasm() { 15 try { 16 const result = await WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject); 17 go.run(result.instance); 18 } catch (err) { 19 // フォールバック: Safari等で失敗した場合 20 const resp = await fetch("main.wasm"); 21 const bytes = await resp.arrayBuffer(); 22 const {instance} = await WebAssembly.instantiate(bytes, go.importObject); 23 go.run(instance); 24 } 25 } 26 loadWasm(); 27 </script> 28 29 <!-- パフォーマンス比較: defer vs async vs 通常読み込み --> 30 <!-- defer: DOMパース完了後に順次実行(推奨) --> 31 <!-- async: ダウンロード完了次第即座に実行(順序不保証) --> 32 <!-- 通常: HTMLパースをブロック(非推奨) --> 33</body> 34</html>
セキュリティ上の理由から、WasmはHTTP経由で提供する必要があります。Goの標準ライブラリだけで、簡単なWebサーバーを立てて動作確認できます。
go1// server.go 2package main 3 4import ( 5 "log" 6 "net/http" 7) 8 9func main() { 10 // WebAssembly用のMIMEタイプを明示的に設定 11 http.HandleFunc("/main.wasm", func(w http.ResponseWriter, r *http.Request) { 12 w.Header().Set("Content-Type", "application/wasm") 13 http.ServeFile(w, r, "main.wasm") 14 }) 15 16 // その他のファイルは通常のファイルサーバーで配信 17 fs := http.FileServer(http.Dir(".")) 18 http.Handle("/", fs) 19 20 log.Println("Server starting on :8080") 21 log.Fatal(http.ListenAndServe(":8080", nil)) 22}
サーバーを起動し、ブラウザでhttp://localhost:8080
にアクセスすると、ブラウザのコンソールに「Hello, WebAssembly from Go!」と表示されます。
Wasmの真価は、GoとJavaScript間で相互に関数を呼び出せることにあります。これは標準のsyscall/js
パッケージを使って実現します。
go1package main 2 3import ( 4 "syscall/js" 5) 6 7func main() { 8 alert := js.Global().Get("alert") 9 alert.Invoke("Hello from Go!") 10 11 // Go から呼べる関数を登録 12 cb := js.FuncOf(func(this js.Value, args []js.Value) any { 13 msg := args[0].String() 14 // ブラウザのコンソールに出力 15 js.Global().Get("console").Call("log", "clicked:", msg) 16 return nil 17 }) 18 js.Global().Set("onClickGo", cb) 19 20 // ブラウザ終了までブロック 21 select {} 22}
go1// interactive.go - JavaScript→Go連携の完全な例 2package main 3 4import ( 5 "syscall/js" 6) 7 8func main() { 9 // Go関数をJavaScriptから呼び出し可能にする 10 js.Global().Set("onClickGo", js.FuncOf(func(this js.Value, args []js.Value) any { 11 if len(args) > 0 { 12 msg := args[0].String() 13 js.Global().Get("console").Call("log", "Go received:", msg) 14 } 15 return nil 16 })) 17 18 // ブラウザ終了までブロック 19 select {} 20}
html1<!-- HTMLでの使用例 --> 2<button onclick="onClickGo('button!')">Go 関数を呼ぶ</button>
この例では、HTMLボタンをクリックするとGo側で定義した関数が実行され、ブラウザコンソールに「Go received: button!」と出力されます。
syscall/js
を介したDOM操作は、JavaScriptに比べて冗長になりがちです。WASI(WebAssembly System Interface)の登場により、ブラウザ以外の環境でも活用が拡大
wasmtime run hello.wasm
コマンドでローカル実行go1// wasi_hello.go 2package main 3import "fmt" 4func main() { 5 fmt.Println("Hello from WASI!") 6}
bash1# WASI対応コンパイル(⚠️ 実験的機能:Go 1.22時点でプレビュー版) 2GOOS=wasip1 GOARCH=wasm go build -o hello.wasm wasi_hello.go 3# Wasmtimeで実行 4wasmtime hello.wasm
⚠️ 重要な注意事項
GOOS=wasip1 は Go 1.22 時点で 実験的機能 です。将来のバージョンで仕様変更や破壊的変更が発生する可能性があります。本番環境での使用は慎重に検討してください。
Component Model: WebAssemblyモジュール間の相互運用性を高める新しい仕様
プレビュー版: wasm-toolsでComponent Model体験
⚠️ 実験的機能
Component Model は現在プレビュー段階の仕様です。本番環境での使用前に最新の仕様変更を確認してください。
Edge Runtime事例:
fastly compute init
でWasmプロジェクト作成wrangler generate
でWasm対応プロジェクト生成サーバーレス環境: AWS Lambda、Google Cloud Functions等でのWasm実行環境整備
GoとWasmを組み合わせることで、これまでサーバーサイドが主戦場だったGoエンジニアの活躍の場がクライアントサイドにまで広がります。特にパフォーマンスが求められる複雑な計算処理や既存のGo資産の再利用といった文脈で、Wasmは強力な選択肢となります。
bash1# 標準Go 2GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o standard.wasm main.go 3wasm-opt -Oz standard.wasm -o standard_opt.wasm 4 5# TinyGo 6tinygo build -o tiny.wasm -target wasm main.go 7wasm-opt -Oz tiny.wasm -o tiny_opt.wasm 8 9# サイズ比較 10ls -lh *.wasm
実測サイズ比較表:
コンパイラ | 最適化前 | wasm-opt後 | 削減率 |
---|---|---|---|
標準Go | 3.2MB | 2.1MB | 34% |
TinyGo | 45KB | 23KB | 49% |
特徴比較:
WebAssemblyは、Goエンジニアにとって新たな可能性を切り開く技術です。ぜひこの機会に、実際に手を動かして体験してみてください。
ブラウザのDevToolsでWebAssemblyデバッガを有効にすると、Goコードのデバッグが可能になります。Chrome DevToolsの「Experiments」から「WebAssembly Debugging」を有効化してください。
html1<!-- コードブロックにaria-label追加例 --> 2<pre aria-label="Go WebAssembly コンパイルコマンド"><code class="language-bash"> 3GOOS=js GOARCH=wasm go build -o main.wasm main.go 4</code></pre> 5 6<!-- ダークモード対応のコントラスト検証 --> 7<!-- 推奨ツール: https://webaim.org/resources/contrastchecker/ -->
私たちGoForceは、バックエンドだけでなく、Wasmのような新しい技術領域に果敢に挑戦する、探究心旺盛なGoエンジニアを応援しています。あなたのその「未知への探求心」と「技術力」を、ユニークで挑戦的なプロジェクトで活かしませんか?
Q: WebAssemblyとJavaScriptの性能差はどの程度ですか? A: 計算集約的なタスクでは、WebAssemblyがJavaScriptより1.5〜3倍高速になることが多いです。ただし、DOM操作が多い場合はJavaScriptの方が効率的です。
Q: 既存のGoライブラリはそのまま使えますか? A: 標準ライブラリの多くは使用可能ですが、ファイルシステムやネットワーク関連の一部機能に制限があります。TinyGoを使用する場合はさらに制限が厳しくなります。
Q: 本番環境での使用は推奨されますか? A: バイナリサイズとロード時間を考慮した上で、計算処理が中心のアプリケーションでは有効です。CDNでの配信とgzip圧縮の併用を推奨します。
最適なGo案件を今すぐチェック!