目次

    ブラウザでGoを動かす!WebAssembly (Wasm) 実践入門
    GoWebAssemblyWasmブラウザフロントエンドsyscall/js実践入門
    202507-09
    GoとWebAssemblyの連携を示すアーキテクチャ図

    ブラウザでGoを動かす!WebAssembly (Wasm) 実践入門

    はじめに:あなたのGoコードがブラウザで直接動く時代

    あなたが書いた、あのパフォーマンスの高いGoのロジックを、サーバーなしで、直接ユーザーのブラウザ上で動かせるとしたら…?これはもはや未来の話ではありません。WebAssembly(Wasm)が、その夢を現実にします。

    この記事では、Wasmの基本からGoプログラムをWasmにコンパイルし、ブラウザ上で動かすまでの「Hello, World」を、実践的なチュートリアル形式で解説します。

    必要な前提環境

    • Go ≥ 1.22 (WebAssembly対応の安定版)
    • Node.js不要 (純粋なGo環境のみで完結)
    • ブラウザ対応表: Chrome 69+, Firefox 65+, Safari 13+, Edge 79+

    WebAssembly (Wasm)とは何か?なぜGoで使うのか?

    WebAssemblyはブラウザで実行可能なバイナリ形式のコードです。高速、安全、ポータブルという特徴を持ちます。

    Goで使うメリット:

    • ロジックの再利用: サーバーとクライアントで、同じGoのビジネスロジックを共有できます。
    • パフォーマンス: 画像処理や複雑な計算など、CPU負荷の高い処理をJavaScriptよりも高速に実行できます。
    • 既存資産の活用: Goで書かれた豊富なライブラリ資産をWebアプリケーションで活用できる可能性があります。

    実践!GoとWasmで「Hello, World」

    実際に手を動かしながら、WebAssemblyの世界を体験してみましょう。

    ステップ①:Goの準備(main.go

    ブラウザのコンソールにメッセージを出力する、シンプルなプログラムから始めます。

    go
    1package 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}

    ステップ②:Wasmへのコンパイル

    GOOS=jsGOARCH=wasm環境変数を使い、.wasmファイルにコンパイルします。

    bash
    1# 基本的なコンパイル
    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ツールによる高度な最適化

    ステップ③:HTMLとJavaScriptの準備(wasm_exec.js

    GoのWasmを実行するためには、「接着剤」の役割を果たすwasm_exec.jsが必要です。

    bash
    1# 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を使った配布形態

    本番環境ではgo:embedを使ってWasmファイルを組み込むことで、単一バイナリでの配布が可能です。

    go
    1// 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を作成します。

    html
    1<!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>

    ステップ④:Webサーバーで実行

    セキュリティ上の理由から、WasmはHTTP経由で提供する必要があります。Goの標準ライブラリだけで、簡単なWebサーバーを立てて動作確認できます。

    go
    1// 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!」と表示されます。


    GoとJavaScriptの連携:相互に関数を呼び出す

    Wasmの真価は、GoとJavaScript間で相互に関数を呼び出せることにあります。これは標準のsyscall/jsパッケージを使って実現します。

    Go→JavaScript: ブラウザAPIの呼び出し

    go
    1package 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}

    JavaScript→Go: イベント駆動の処理

    go
    1// 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}
    html
    1<!-- HTMLでの使用例 -->
    2<button onclick="onClickGo('button!')">Go 関数を呼ぶ</button>

    この例では、HTMLボタンをクリックするとGo側で定義した関数が実行され、ブラウザコンソールに「Go received: button!」と出力されます。


    現状の課題と将来性

    • 課題:
      • バイナリサイズ: Goが生成するWasmファイルは、現時点では数MB〜数十MB単位と大きく、初期ロード時間に課題があります。TinyGoを使用すると2〜3MBまで圧縮できる場合がありますが、API対応は限定的です。
      • DOM操作: syscall/jsを介したDOM操作は、JavaScriptに比べて冗長になりがちです。
    • 将来性:
      • WASI(WebAssembly System Interface)の登場により、ブラウザ以外の環境でも活用が拡大

        • 試してみる: Wasmtimewasmtime run hello.wasm コマンドでローカル実行
        • オンラインデモ: WebAssembly Studioでブラウザ上でWasm開発体験
        • WASI対応の最小コード例:
        go
        1// wasi_hello.go
        2package main
        3import "fmt"
        4func main() {
        5    fmt.Println("Hello from WASI!")
        6}
        bash
        1# 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事例:

      • サーバーレス環境: AWS Lambda、Google Cloud Functions等でのWasm実行環境整備


    まとめ:Goの新たなフロンティアに挑戦する

    GoとWasmを組み合わせることで、これまでサーバーサイドが主戦場だったGoエンジニアの活躍の場がクライアントサイドにまで広がります。特にパフォーマンスが求められる複雑な計算処理や既存のGo資産の再利用といった文脈で、Wasmは強力な選択肢となります。

    今すぐ試せる実践ステップ

    1. 既存のGoライブラリをWasm化してみる
    2. バイナリサイズ最適化のベンチマークを取る
    3. TinyGoとの性能・サイズ比較を実施する

    TinyGoとの比較ベンチマーク例

    bash
    1# 標準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後削減率
    標準Go3.2MB2.1MB34%
    TinyGo45KB23KB49%

    特徴比較:

    • 標準Go: フル機能、大きなバイナリサイズ
    • TinyGo: 軽量、一部API制限あり、起動高速

    WebAssemblyは、Goエンジニアにとって新たな可能性を切り開く技術です。ぜひこの機会に、実際に手を動かして体験してみてください。

    デバッグのコツ

    ブラウザのDevToolsでWebAssemblyデバッガを有効にすると、Goコードのデバッグが可能になります。Chrome DevToolsの「Experiments」から「WebAssembly Debugging」を有効化してください。

    アクセシビリティ向上のためのコード例

    html
    1<!-- コードブロックに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エンジニアを応援しています。あなたのその「未知への探求心」と「技術力」を、ユニークで挑戦的なプロジェクトで活かしませんか?


    参考文献

    関連記事

    FAQ

    Q: WebAssemblyとJavaScriptの性能差はどの程度ですか? A: 計算集約的なタスクでは、WebAssemblyがJavaScriptより1.5〜3倍高速になることが多いです。ただし、DOM操作が多い場合はJavaScriptの方が効率的です。

    Q: 既存のGoライブラリはそのまま使えますか? A: 標準ライブラリの多くは使用可能ですが、ファイルシステムやネットワーク関連の一部機能に制限があります。TinyGoを使用する場合はさらに制限が厳しくなります。

    Q: 本番環境での使用は推奨されますか? A: バイナリサイズとロード時間を考慮した上で、計算処理が中心のアプリケーションでは有効です。CDNでの配信とgzip圧縮の併用を推奨します。

    会員登録はこちら

    最適なGo案件を今すぐチェック!

    会員登録

    生年月日 *

    /

    /

    Go経験年数 *

    /

    利用規約プライバシーポリシーに同意してお申し込みください。