22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで始めるWebAssembly入門 ─ ブラウザとサーバーで動かす実践ガイド

Last updated at Posted at 2025-12-11

はじめに

わーい、アドカレだー!お祭りみたいで楽しいですね。
というわけで、今回は Wasm についての入門記事を書いてみました。

「Wasmを知らない」「Wasmって言葉は知っているけど触ったことはない」、そんな方が対象です!最後まで読んでいただけたら嬉しいです!

🚀 本記事執筆の背景
先日参加した Go Conference 2025 にて、Go言語でのWebAssembly活用についてのお話をたくさん伺ったので、自分でも使ってみることにしました!

本記事のゴールは以下の 3つ です。

  • Wasm を知る
  • Go での Wasm の扱いについて理解する
  • Go から Wasm をビルドして実行するまでの流れを掴む

そもそも WebAssembly (Wasm) とは

WebAssembly(Wasm: わずむ)とは、スタックベースの仮想マシン向けに設計された、バイナリ形式の命令セットのことです。Wasm はさまざまなプログラミング言語の「移植性の高いコンパイル先」として設計されており、Web ブラウザやサーバ環境など、クライアント/サーバのどちらでも実行できるようになっています。

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

うーん、公式の説明が難しい。
すごく簡単に言うと、Wasmは、さまざまなプログラミング言語で書いたコードを、さまざまな実行環境で動くようにした実行形式のことです。Wasm に変換すると、そのプログラムはブラウザでもサーバーでも同じように動かせます。

なお、Wasm形式のバイナリファイルの拡張子は.wasmです。

Wasm の歴史

Wasm誕生から現在までの流れをざっくりと整理します。

もともとWebブラウザでは JavaScript しか実行できませんでしたが、「重い処理を高速に動かしたい!」というニーズからWasmが誕生し、C, C++, Rust, Go などで書かれたコードがブラウザ上でも高速・安全に実行できるようになりました。

さらに「ブラウザ以外でも Wasm を動かしたい!」というニーズから、OS 上で Wasm を実行するための標準 API である WASI (WebAssembly System Interface) が誕生しました。

その結果、いまでは Wasm は「ブラウザ技術」の枠を超え、「どこでも動く軽量バイナリフォーマット」という位置づけになってきています。

Wasmの活用シーン

Wasm の代表的な活用シーンとして、ブラウザ上で高負荷な処理を高速に実行する分野があります。たとえば、ゲームの3D描画、画像・動画編集、音声処理など、従来はネイティブアプリでしか難しかった処理を、Wasm によってブラウザ上でスムーズに動かせるようになりました。

また近年では、サーバーサイドでの利用にも注目が集まっています。エッジ環境での軽量・高速な実行基盤としての活用や、安全なプラグインシステムの実装など、ブラウザの枠を超えて用途が拡がっています。

📝 V8エンジンとWasm
V8エンジン(ChromeのJavaScriptエンジン)は WebAssembly を標準サポートしているため、V8エンジンを利用している Node.js ランタイムでは追加の依存を入れることなく Wasm を直接実行できます。AWS Lambda などの Node.js ランタイムを含む多くの実行環境で、Wasm をそのまま使うことができます。

Wasmの特徴

Wasmの主な特徴を、以下に4つ挙げます。

  1. 効率的で高速
  2. 安全
  3. いろいろな言語から作ることができる
  4. Wasmランタイムがあればどこでも動く

1. 効率的で高速

コンパクトなバイナリ形式でエンコードされるため、JavaScriptよりも高速(ほぼネイティブ速度)で実行できる。

2. 安全

サンドボックス化されたメモリ空間で動作するよう規定されているため、プログラムが他の部分に勝手にアクセスできない(メモリ安全)。これにより、不正なアクセスやデータの改ざんを防ぐことができる。

3. いろいろな言語から作ることができる

C, C++, Rust, Goなど多くの言語からWasm形式のバイナリを生成できる(言語非依存)。特定の言語に縛られず、好きな言語で開発したプログラムをWasm形式に変換できる。

4. Wasmランタイムがあればどこでも動く

Wasmは、どのプログラミング言語で作ったかに関係なく、Wasmランタイム(Webブラウザやサーバ上の実行環境)があればOSやデバイスを問わず同じように動作する(プラットフォーム非依存)。ひとつのバイナリをいろんな環境で動かすことができる。

📝 どこでも動作するコード
JVM(Java Virtual Machine)も、スタックベースの仮想マシンです。JVMは "Write once, run anywhere" を実現し、JavaバイトコードをどんなOSでも実行できます。
JVMによってJavaなどの言語をOSに依存せずに動かせるように、Wasmは C, C++, Rust, Goなど色々な言語に対応し、Webを含むあらゆる環境で高速・安全に動かせる新世代のバイナリ命令セットと言えます。

GoとWasmの歴史

ここで、GoのWasm対応の歴史について整理していきます。

image.png

2018 (Go 1.11)ブラウザ上でのWasm実行に対応

Go1.11で、初めてWasmが公式サポートされました。
この時点では ブラウザ専用の Wasm で、ターゲットはGOOS=js GOARCH=wasmのみでした。
GoとJavaScriptを連携する仕組みsyscall/jsが導入され、GoからJavaScriptを経由してブラウザの機能(DOM、Canvas、Fetch など)が利用できます。

これにより、Go でフロントエンドコードが書けるようになりました。

2023 (Go 1.21)OS上でのWasm実行(WASI)に対応

Go1.21では、WebAssembly System Interface(WASI)Preview 1 が公式サポートされました。
ターゲットGOOS=wasip1 GOARCH=wasmにより、ブラウザだけでなく OS 上でも Wasm を実行できるようになりました。WASI p1 は「ブラウザ外で Wasm を動かすための最低限の API 仕様群」であり、ファイル I/O・標準入出力・時計などの基本機能が提供されています。

これにより、Goで書いたWasmがブラウザ外の領域(サーバ、CLI、エッジ環境など)にも進出しました。

2025 (Go 1.24)Wasm プラグイン化(関数エクスポート)に対応

Go1.24では、go:wasmexport が導入されました。
Go の関数を Wasm モジュールのエクスポート関数として、ホスト側から直接呼び出せるようになりました。

これにより、Wasmをモジュール化し外部ホストからプラグインとして利用できるようになりました。

ブラウザでのWasm

まず、ブラウザでのWasmの動作について見ていきます。
ブラウザでのWasmは、 JavaScript の代替ではなく、あくまで JavaScript の補助的な役割として動きます。ブラウザでのロジックや計算処理などはWasm単独で高速に実行することができますが、UI操作(DOM操作)やイベント処理などは JS を経由して実行されます。

Goで書かれたプログラムから JS にアクセスするには syscall/js パッケージを利用します。
syscall/js パッケージは、js/wasm アーキテクチャを使用する際に WebAssembly ホスト環境へのアクセスを提供します。

Package js gives access to the WebAssembly host environment when using the js/wasm architecture.

ブラウザでWasmを動かすための流れとしては、まずGoなどの対応する言語でプログラムを作成します。次に、そのソースコードをWasmターゲットのコンパイラを使ってWebAssemblyバイナリ(.wasmファイル)に変換します。その後、Webページ上のJavaScriptからこのWasmバイナリを読み込み、インスタンス化することで、Wasmの関数をJavaScriptから呼び出すことができ、ブラウザ上で高速に実行されます。

image.png

ブラウザでWasmを動かすために必要なもの

Goで書いたコードをブラウザ上でWasmとして実行するには、最低限、次の3つのファイルが必要です。

  1. Wasmファイル
  2. HTMLファイル
  3. ランタイムブリッジ

1. Wasmファイル

Wasmファイルは、Goなどの言語で書かれたプログラムをWasmターゲット向けにビルドして作成します。
今回は次のmain.goを使用します。

main.go
//go:build js && wasm

package main

import (
	"syscall/js"
)

func main() {

    // document オブジェクト
	doc := js.Global().Get("document")

    // HTML 内の id="count" の要素を取得
	countEl := doc.Call("getElementById", "count")

    // HTML 内の id="click-btn" の要素を取得
	btn := doc.Call("getElementById", "click-btn")
    
	count := 0

    // count の内容を countEl の textContent に反映する
	update := func() { countEl.Set("textContent", count) }

    // Go の関数を JavaScript から呼び出せる関数にする
	click := js.FuncOf(func(js.Value, []js.Value) interface{} {
		count++
		update()
		return nil
	})
	defer click.Release()

	update()

    // ボタンにクリックイベントを設定
	btn.Call("addEventListener", "click", click)

    // プログラムが終了しないよう待機
	select {}
}

この Go プログラムは、画面上のボタン(click-btn)をクリックすると表示されるカウンター(count)が増えていくという、とてもシンプルな「クリックカウンター」を実装しています。
ポイントは以下の3つです。

  • syscall/js パッケージでは、js.Global() を使って、JSのグローバルオブジェクトにアクセスする ( JSでいうところのglobalThis )。このグローバルアクセスを通じて、Go は DOM や Web API に到達できる
  • doc := js.Global().Get("document")でdocumentオブジェクトを取得している( JSでいうところのconst doc = globalThis.document; )。また、doc.Call()で DOM 要素を取得、js.FuncOf()ではGoのclick関数をJSの関数として登録している
  • 最後のselect {}により、永遠に待機し続けるようにしている。Wasm の main() は終了するとプログラム自体が止まるため、この無限待機をすることで、JS のイベントループと同じ動作を維持している

上記のプログラムmain.goを、次のコマンドでビルドします。ビルド時のターゲットは、GOOS=js GOARCH=wasmを指定します。

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

ビルドすると、Wasm形式のバイナリファイル main.wasmが生成されます。

2. HTMLファイル

HTMLファイルは、ブラウザで開く際のエントリーポイントとなります。そのため、HTML内でランタイムブリッジ(wasm_exec.js)と、Wasmファイル(main.wasm)を読み込む必要があります。

index.html
<html>
<head>
</head>
<body>
  <main>
    <h1>クリックカウンター</h1>
    <p class="hint">押すごとにカウントが進みます</p>
    <p id="count" class="count">0</p>
    <button id="click-btn">クリック</button>
  </main>
  <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>
</body>
</html>

ここでのポイントは以下です。

  • <script src="wasm_exec.js"></script>でGo/JS間のランタイムブリッジであるwasm_exec.jsを読み込んでいる。また、new Go() で Go ランタイムインスタンスを作成し、Wasm の読み込み・インスタンス化・実行をしている

3. ランタイムブリッジ

index.htmlで読み込んでいる wasm_exec.js は、Go で生成した Wasm をブラウザ上で動かすためのランタイムブリッジです。JS と Wasm 間の値の変換・関数呼び出しの仲介を行ってくれます。

Go からビルドした Wasm は、このランタイムブリッジがないと動作しません。

wasm_exec.js は Go をインストールすると標準で入っています(Goの公式配布物に含まれている)。パスは環境によって異なりますが、一般的には ${GOROOT}/lib/wasm/wasm_exec.jsに置かれています。

ブラウザでWasmを動かす

それでは実際に、ブラウザ上でWasmを最小サンプルで動かしてみます。
Webへの公開ディレクトリに3つファイルを配置すれば、それだけで動作します。

.
 ├── index.html
 ├── main.wasm
 └── wasm_exec.js

作成したクリックカウンターを Github Pages で公開してみました。以下がクリックカウンターのサイトです。

image.png

ブラウザの開発者ツールなどを使うと、3つのファイルだけで構成されていることがわかるはずです。

image.png

またブラウザでは次のように、バイナリ形式のWasmをテキスト形式に変換してくれるため、ソースコードの閲覧・デバックをすることができます(WebAssembly text format)。

image.png

以上が、ブラウザでWasmを動作させるまでの流れになります。

ブラウザ外でのWasm

次は、ブラウザ外 (OS) でWasmを動作させてみます。
サーバなどのOS上でWasmを動作させる際には、Wasmランタイムを用意します。WASI (WebAssembly System Interface) を利用して、OS機能との連携を行います。

OSでWasmを動かすための流れとしては、まずGoなどの対応する言語でプログラムを作成し、コンパイラを使ってWebAssemblyバイナリ(.wasmファイル)に変換します。変換したWasmバイナリは、OS上のWasmランタイム(たとえばWasmerやWasmtime、wazeroなど)によって読み込みます。ランタイムは、Wasmバイナリの中身を解析して、OSの機能(ファイルアクセスやネットワークなど)への安全なインターフェースを提供しながらプログラムを実行します。

image.png

WASI(WebAssembly System Interface)とは

WebAssembly System Interface(WASI: わじい)とは、Wasmで作ったソフトウェアがOSの機能を利用できるようにするための「標準API仕様群」のことです。どの言語から生成した Wasm でも、同じインターフェースで OS 機能(ファイル操作・時間・環境変数など)にアクセスするための標準 API を提供しています。これにより、Wasm アプリをいろいろな環境に持ち運べるようになります。

The WebAssembly System Interface (WASI) is a group of standards-track API specifications for software compiled to the W3C WebAssembly (Wasm) standard. WASI is designed to provide a secure standard interface for applications that can be compiled to Wasm from any language, and that may run anywhere—from browsers to clouds to embedded devices.

📝Tiny Go と Wasm について
2025年12月時点現在、WASI は Preview 2 (プレビュー版 2)までリリースされていますが、Go は WASI Preview 1 のみ対応しています。対して TinyGo は、Preview 2 に実験的ではあるものの対応しており、Go 本体よりも先行している状況です。TinyGo は「軽量で高速なGoコンパイラ」であり、Wasm 向けのコード生成を得意としています。そのため、WebAssembly を利用する際の有力な選択肢のひとつとなっています。 - WebAssembly | TinyGo

ブラウザ外でWasmを動かすために必要なもの

ブラウザ外 (OS) でWasmを実行するには、次の2つが必要です。

  1. Wasmファイル
  2. Wasmランタイム

1. Wasmファイル

ブラウザ外でWasmを動かす場合も、まずは Wasmファイル を用意します。
Goで書いたコードを、ターゲットGOOS=wasip1 GOARCH=wasmを指定してビルドすることで、Wasmファイル生成します。

hello.go
package main

import "fmt"

func main() {
    fmt.Printf("Hello, world!")
}

次のビルドコマンドでビルドします。ビルド時のターゲットは、GOOS=wasip1 GOARCH=wasmを指定します。

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

2. Wasmランタイム

Wasmファイルがあっても、それだけでは実行できません。Wasmランタイムが必要です。
Wasmランタイムは、Wasmバイナリを読み込み、メモリ管理やシステムコール(WASIなど)を担当し、OS上での実行環境を提供します。簡単に言うと、「Wasmプログラムを実行するための仮想マシン」です。

Wasmランタイムが WASI に対応していれば、Wasmファイル内でファイルの読み書き・標準入出力・環境変数取得などの機能が使えます。

さまざまなWasmランタイムが存在しますが、今回は wazero を使用します。
wazero は、Pure Goで書かれた唯一の Wasm ランタイムです。依存性ゼロである (CGOにも依存しない) ため、Goの移植性を活かすことができます。また、Goアプリにそのまま組み込むこともできます。

wazero is a WebAssembly runtime, written completely in Go. It has no platform dependencies, so can be used in any environment supported by Go.

ランタイムによっては、デバッグ機能、セキュリティ設定、独自APIの利用、専用の設定ファイルなども用意されています。

ブラウザ外でWasmを動かす

Wasmランタイム(wazero)を使用してWasmを実行する方法としては、以下の2つがあります。

  1. WasmをCLIで動かす
  2. WasmをGoコード内に埋め込んで動かす

1. WasmをCLIで動かす

wazero CLI を入手して、任意の Wasm バイナリを実行できます。以下は先程ビルドした hello.wasm を wazero で実行している様子です。

$ go install github.com/tetratelabs/wazero/cmd/wazero@latest
$ ./bin/wazero run hello.wasm
Hello, world!

hello.wasm が実行され、Hello, world! と標準出力されていることが確認できます。

2. WasmをGoコード内に埋め込んで動かす

wazero を使ってWasmをGoコードに埋め込み、プラグインとして関数だけを呼び出すことができます。

ここでは、add(x, y) というシンプルな関数を Wasm で実装し、ホスト側の Go からその関数を呼び出してみます。

ディレクトリ構成
.
├── guest
│   └── add.go  // Wasm にビルドされる側
└── host
    ├── add.wasm  // ビルドされた Wasm バイナリ
    └── main.go  // Wasm を呼び出す側
  • guest/ ... Wasm にビルドされる側(プラグイン本体)
  • host/ ... Go として動作し、Wasm を読み込んで実行する側

ゲスト側

add.go
package main

//go:wasmexport add
func add(x, y uint32) uint32 {
	return x + y
}

// main is required for the `wasi` target, even if it isn't used.
// See https://wazero.io/languages/tinygo/#why-do-i-have-to-define-main
func main() {}

ポイントは以下の2つです。

  • //go:wasmexport add ディレクティブにより、add 関数が Wasm モジュールのエクスポート関数として公開される。ホスト側はこの名前 "add" で関数を呼び出す
  • wasi ターゲット向けにビルドする場合、エントリーポイントとして main() が必須なため、実際に使わなくても定義だけ入れる必要がある

この add.go を Wasm にビルドすると add.wasm が生成されます。

$ GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o host/add.wasm guest/add.go

このとき、-buildmode=c-shared ビルドフラグを使用します。このビルドフラグを付けることで、Wasm が「実行して終わるプログラム(Command モード)」ではなく「関数として呼び出せるライブラリ(Reactor モード)」として機能するようになります。

ホスト側

main.go
package main

import (
	"context"
	_ "embed"
	"fmt"

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

//go:embed add.wasm
var addWasm []byte

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

	// Create a Wasm runtime, set up WASI.
	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)
	wasi_snapshot_preview1.MustInstantiate(ctx, r)

	// Configure the module to initialize the reactor.
	config := wazero.NewModuleConfig().WithStartFunctions("_initialize")

	// Instantiate the module.
	wasmModule, _ := r.InstantiateWithConfig(ctx, addWasm, config)

	// Call the exported function.
	fn := wasmModule.ExportedFunction("add")
	var a, b int32 = 1, 2
	res, _ := fn.Call(ctx, api.EncodeI32(a), api.EncodeI32(b))
	c := api.DecodeI32(res[0])
	fmt.Printf("add(%d, %d) = %d\n", a, b, c)

	// The instance is still alive. We can call the function again.
	res, _ = fn.Call(ctx, api.EncodeI32(b), api.EncodeI32(c))
	fmt.Printf("add(%d, %d) = %d\n", b, c, api.DecodeI32(res[0]))
}

ポイントを整理すると、

  • //go:embed add.wasm によって、ビルド済みの add.wasm を Go のバイナリ内に埋め込んでいる。これにより、実行時にファイルを読む必要がなく、配布が楽になる
  • wazero.NewRuntime, wasi_snapshot_preview1.MustInstantiate では、Wasm を実行するためのランタイムを生成し、WASI を有効にしている。
  • WithStartFunctions("_initialize") では、初期化関数を起動時に呼ぶよう設定してる。(「Reactor モード」(繰り返し呼び出せるライブラリ的な使い方)をしているため)
  • ExportedFunction("add") で関数を取得し、fn.Callで実行している

これで、ホスト側の Go プログラムから、Wasm の add 関数をプラグインとして呼び出すことができます。


ホスト側のhost/main.goを Go で実行します。すると、Wasm の add 関数がうまく機能していることが確認できます。

$ go run host/main.go 
add(1, 2) = 3
add(2, 3) = 5

以上が、ブラウザ外でWasmを動作させるまでの流れになります。

さいごに

今回は、Wasmの基本から、Goによるブラウザ・OS上での実行方法までを一通り学んでみました。実際に手を動かしてみることで、「Wasmがどこでも動く軽量・高速なバイナリ形式」という特徴や、その応用範囲の広さを実感できます。

なお、Wasm自体は急速に進化しており、2025年9月にリリースされた Wasm 3.0 では性能や機能面が強化され、今後の活用分野がますます広がっていきそうです。

2026年には WASI 1.0rc がリリース予定なので、そのタイミングで再度キャッチアップすると面白いと思います。

image.webp

技術の進化を楽しみながら、Wasmの可能性がどう広がっていくのか、これからも注目していきたいと思います。

最後までご覧いただき、ありがとうございました!

参考

本記事を書くにあたり、公式ドキュメントや各種ブログ、コミュニティによる有益な記事を参考にさせていただきました。先人の皆様の知見に、心より感謝申し上げます。

本記事では最新技術動向や将来のリリース予定を含めて紹介していますが、実際のリリース状況・仕様については公式サイト等をご確認ください。

22
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?