Go x WebAssembly ってどんな?


この記事は Wano Group Advent Calendar 2019 の12日目の記事となります。

Advent Calendar もそろそろ折り返しですね🎅🎁

GoとWebAssembly

WebAssemblyとは何ぞって話は詳しくはしませんが、ブラウザが読めるアセンブリのコード形式のことです。(詳しくはここ
特徴としてjavascriptに比べ高速という点が挙げられ、jsの補完的な立ち位置で、モダンブラウザは概ねサポートしています。

Goではv1.11よりWebAssemblyがサポートされるようになり、進行形で様々な改善がされているみたいです。
Officialに色々書いてあります。

ということで勉強がてらGo x WebAssemblyをかなり簡単にですが触ってみて、所感を書こうと思います。

Hello Worldしてみる

公式のGetting Started を参考にやってみます。

1. WebAssemblyバイナリ

スクリプトは特に何も意識することないですね。 これをWebAssembly形式にビルドします。
さすがビルド速いです

test.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}
$ GOOS=js GOARCH=wasm go build -o test.wasm

2. html・jsファイル

すでにGoのインストール時にサンプルがあるのでそれをコピーして使用します。

$ cp /usr/local/Cellar/go/1.12.5/libexec/misc/wasm/wasm_exec.{js,html} ./

3. Webサーバー起動

Webサーバーを立てておきます。

server/server.go
package main

import "net/http"

func main() {
    http.ListenAndServe(":8080", http.FileServer(http.Dir("../")))
}
$ go run server/server.go

4. ブラウザで確認

Run ボタンをクリックすると以下のようにコンソールに出力されます

ディレクトリ構成は以下になります。

|--server
| |--server.go
|--test.go
|--test.wasm
|--wasm_exec.html
|--wasm_exec.js

test.wasm を実行するためのスクリプトが wasm_exec.js で、それと test.wasmwasm_exec.html が呼んでいるという流れになります。

JSで簡単な操作をしてみる

👆のファイルを少々手直しして、JSっぽいことをしてみます。

ボタンクリック時に背景色を変更させます。

sample.html
<!doctype html>
<html>

<body>

<script src="wasm_exec.js"></script>
<script>
    const go = new Go();
    let mod, inst;
    WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then(async (result) => {
        mod = result.module;
        inst = result.instance;

        await go.run(inst);
        inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
    }).catch((err) => {
        console.error(err);
    });
</script>

<input type="text" id="colorCodeInput">
<button type="submit" id="colorChangeButton">背景色を変更する</button>
</body>

</html>

👇ゴリッゴリにjs叩いてます

sample.go
package main

import (
    "syscall/js"
)

func main() {
    document := js.Global().Get("document")

    body := document.Call("getElementsByTagName", "body").Index(0)
    input := document.Call("getElementById", "colorCodeInput")

    cb := js.FuncOf(func(this js.Value, args []js.Value) interface{}{
        body.Get("style").Set("backgroundColor", input.Get("value").String())
        return nil
    })
    document.Call("getElementById", "colorChangeButton").Call("addEventListener", "click", cb)

    <-make(chan struct{}, 0)
}

こんな感じになります。

Goで書いたコードがクライアントサイドで動くのが新鮮✨

画像を使ってみる

canvasを使っていっぱいGopher君を出してみます。

<script> の部分は👆と同じです。

sample.html
<!doctype html>
<html>
    <body>
...
        <canvas width="1500" height="1000" id="sample"></canvas>
    </body>
</html>

sample-2.go
package main

import (
    "math/rand"
    "syscall/js"
    "time"
)

var (
    document = js.Global().Get("document")
    img      = js.Global().Call("eval", "new Image()")
)

func main() {
    img.Set("src", "./image/gopher.png")

    body := document.Call("getElementsByTagName", "body").Index(0)
    body.Call("addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        draw()
        return nil
    }))

    <-make(chan struct{}, 0)
}

func draw() {
    canvas := document.Call("getElementById", "sample")
    ctx := canvas.Call("getContext", "2d")

    rand.Seed(time.Now().UnixNano())
    x := rand.Intn(1000)
    rand.Seed(time.Now().UnixNano() + 100000)
    y := rand.Intn(1000)

    ctx.Call("drawImage", img, x, y)
}


* The Go gopher(Gopherくん)は、Renée Frenchによってデザインされました。

このくらいの処理だとJSと体感差はなく、もっと重い画像/動画処理や物理演算などでないとパフォーマンスの差は感じられなそうです。
スマホくらいのcpuだと若干ラグがあったりするかも

まとめ

  • shimmerのような画像処理など実装してみたかったが、使えるパッケージに制約があったりで結構しんどそうだったので今回はここまで

  • コンパイルは早いし、別途インストールする必要もないので非常に手軽

  • サーバー側をGoで書いている場合、サーバー側で定義している値・ロジックなどを使用できて、クライアント側に流出しないのは強みになりそう

  • syscall/js 叩きすぎるとパフォーマンスがもの凄く下がるので、使い方は慣れが必要そう

  • 現状GCがなかったり、デバッグがかなりしづらかったり、必要な場面に遭遇しづらいなどなどありますが、今後Web領域以外での使用や、言語側の改善も含めて追っておいて損はなさそう