WebAssemblyでGoの関数をブラウザのJavaScriptから呼び出す

golangwasmjavascript

ボタンを押すとGoで実装された関数が呼ばれその中でテキストの値が更新される。

goの関数による値の更新
$ go version
go version go1.18 darwin/amd64

syscall/js packageでjs側の変数を参照したり関数を呼び出すことができる。 Go 1.12で破壊的変更があり NewCallback() は FuncOf() に変更された。 返り値は js.ValueOf() に対応している型である必要がありstructを返すとエラーになる。

package main

import (
	"strconv"
	"syscall/js"
)

func increment(this js.Value, args []js.Value) any {
	counter := js.Global().Get("document").Call("getElementById", "counter")
	counterValue, err := strconv.ParseInt(counter.Get("textContent").String(), 10, 64)
	if err != nil {
		return map[string]any{"error": err.Error()}
	}
	counterValue += int64(args[0].Int())
	counter.Set("textContent", counterValue)
	return map[string]any{"message": counterValue}
}

func main() {
	js.Global().Set("goIncrement", js.FuncOf(increment))
	select {} // keep running
}

GOOS=js, GOARCH=wasm でビルドすると .wasmファイルが生成される。 Goの.wasmファイルはサイズが大きくなる傾向があり、このファイルは1.3MBとなっている。 TinyGoでビルドすると165KBと大幅に小さくなる。

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

公式のリポジトリにある wasm_exec.js を読み込み WebAssembly.instantiateStreaming で.wasmファイルをコンパイルしロードする。

<html>

<head>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
        function increment(value) {
            ret = goIncrement(value)
            console.log(ret)
        }
    </script>
</head>

<body>
    <div id="counter">0</div>
    <button onClick="increment(1)">+1</button>
</body>

これらを配信するサーバーを立ち上げて動作確認する。

$ npx http-server
$ open localhost:8080/index.html

参考

Go WebAssembly Tutorial - Building a Calculator Tutorial | TutorialEdge.net

javascript - Golang’s syscall/js js.NewCallback is undefined - Stack Overflow

How do WebAssembly binaries compiled from different languages compare in size? - Stack Overflow