Go+WebAssemblyで時刻表示を実装してみた
前々からWebAssemblyでなんか作ってみたいと思っていたので、今回はJSをなるべく書かないように時刻表示の実装をやってみようと思いました。
説明よりもコードを見たい方はこちらを御覧ください
完成したのはこちらです。
WebAssembly
WebAssembly は最近のウェブブラウザーで動作し、新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードです。基本的に直接記述ではなく、C、C++、Rust 等の低水準の言語にとって効果的なコンパイル対象となるように設計されています。
https://developer.mozilla.org/ja/docs/WebAssembly/Concepts#WebAssembly_はどのようにウェブプラットフォームに適合するのか
ここではC, C++, Rustが紹介されていますが、Goでも対応しています。
Goの場合は、GoからWasmを呼び出すためにwasm_exec.js
を用意する必要があります。
もちろん1から記述必要はなく、Goをインストール時に含まれています。
準備
- まずは適当なディレクトリを作って、
go mod
とmain.go
を作っておきましょう
package main
import "fmt"
func main(){
fmt.Println("Hello World!")
}
-
go build
をしてwasmを作ってみましょう。
単純にbuildしてしまうと、バイナリファイルができてしまうので、環境変数を指定してあげてください。
GOOS=js GOARCH=wasm go build -o main.wasm
これでwasmは完成です!
- 次の上記で記述したように
wasm_exec.js
を用意します
wasm_exec.js
はGOROOT配下にあるので、カレントディレクトリにコピーします。
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
- このままではwasmは動かないので
index.html
に以下のように記述しましょう。
<html>
<head>
<meta charset="utf-8"/>
<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>
</head>
<body>
</body>
</html>
スクリプトタグでは、先程作成したmain.wasm
をfetchしてGoを実行するようにしています。
-
goexec実行後
http://localhost:8080
を開き、consoleを確認しこのように表示されていれば完成です🎉
goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'
今回はもう少し凝ったものを作りたいのでこのようなディレクトリ構成にしました。
ディレクトリ構成
.
├── README.md
├── build.go // wasmのメインとなるもの
├── docs
│ ├── build.wasm // go buildして生成されたもの
│ ├── index.html
│ └── wasm_exec.js
├── go.mod
├── go.sum
└── main.go // httpServer
index.html
は上記で記述したものを使っています。変更点はfetch対象をbuild.wasm
に変更してbodyに<div id="clock">
を追加しました。
また、goexecを使わないでhttpServerを作成しました。
package main
import (
"log"
"net/http"
)
func main() {
port := "8080"
http.Handle("/", http.FileServer(http.Dir("./docs/")))
log.Printf("Listen on port: %s", port)
http.ListenAndServe(":"+port, nil)
}
Go標準ライブラリで実装
手始めにGo標準ライブラリtime
で実装してみましょう。
build.go
に以下の記述をします。
package main
import (
"syscall/js"
"time"
)
func clock() {
now := time.Now()
date := js.Global().Get("Date").New(now.Unix() * 1000)
js.Global().Get("document").Call("getElementById", "clock").Set("innerHTML", date)
}
func main() {
clock()
select {}
}
注意するべき点は下記の部分です。
date := js.Global().Get("Date").New(now.Unix() * 1000)
JSではUNIXの時間をミリで表示しているので1000かけて上げる必要があります。
できあがったものがこちら
ただこれだと味気ないのでもう少しリアルタイムに表示させましょう。
リアルタイム実装
build.go
をこのように変更します。
package main
import (
"syscall/js"
)
func clock(this js.Value, args []js.Value) interface{} {
date := js.Global().Get("Date").New().Call("toLocaleString").String()
js.Global().Get("document").Call("getElementById", "clock").Set("innerHTML", date)
return nil
}
func main() {
js.Global().Call("setInterval", js.FuncOf(clock), "200")
select{}
}
ライブラリはsyscall/jsを使います。
clockで実処理を行っています。一行づつ解説していきます。
date := js.Global().Get("Date").New().Call("toLocaleString").String()
js.Global()
はwindowからいろいろなオブジェクトに対してアクセスしています。Get()
を使ってDateオブジェクトを取得してあげます。Dateはjs同様インスタンスを生成して上げる必要があるので、New()
を記述してあげます。Call()を使うことによってJavaScriptの呼び出しを行ってあげます。
js.Global().Get("document").Call("getElementById", "clock").Set("innerHTML", date)
js.Global().Get("document")
は上記と同様です。今回はdocumentを取得し、getElementByIdで初めの方に作った<div id="clock">
を指定してあげます。
ファイルサイズについて
wasmのファイルサイズは以下のように2Mを下回っています。(記述量が少ないのもありますが、、、)
ただ、Goの場合2~10MB以上に変動するのでこれ以下に抑えたい場合は、手動でzipしてあげるか、TinyGoを使うのを推奨しています。
- Manually compress the .wasm file.
- Use TinyGo to generate the Wasm file instead.
https://github.com/golang/go/wiki/WebAssembly#reducing-the-size-of-wasm-files
Discussion