本稿では、筆者が普段使っているツールの一つである、 GopherJS の基本的な使い方について説明します。
GopherJS は Go で書いたプログラムを JavaScript に変換するソフトウェアです。生成した JavaScript は、 Web ブラウザはもちろん、 node.js で実行することが出来ます。 Web アプリケーションを書かなければならないが、どうしても Go で書きたいという、 Go プログラマーによくある (?) お悩みにうってつけです。
変換結果はそこそこ素直な読みやすい JavaScript になります。 asm.js に変換する Emscripten や、バイナリ形式の Web Assembly とは異なるアプローチです。 GopherJS のアプローチの場合、デバッグやプロファイルはしやすいですが、ファイルサイズやパフォーマンスには若干難があります。
なお本稿では node.js よりはブラウザ用途を中心に解説します。
使い方
GopherJS のコマンド gopherjs
は go get
でインストールできます。
go get -u github.com/gopherjs/gopherjs/...
ビルドしたいパッケージを gopherjs
コマンドをパッケージを指定して実行するだけです。
gopherjs build github.com/yourname/yourproject
main パッケージならば、 js ファイルと map ファイルがその場に生成されます。 build
以外にもサブコマンド gopherjs install
や gopherjs get
などがあり、 go
コマンドのそれと似ています。詳しくは gopherjs help
を参照してください。
インストールせずに試したい場合は、 GopherJS Playground で実験することが出来ます。変換結果は得られませんが、ほとんどすべての Go プログラムを GopherJS によってブラウザで実行できることが確認できると思います。
機能
出来ること
- Go の言語機能すべて
- ほぼすべての標準ライブラリ。 os パッケージなどはブラウザではなく node で使われることを想定しています。
- 任意の JavaScript 関数の呼び出し
- 数値、文字列、バイト配列などの一部の型の相互変換
出来ないこと
簡単な例
実際に簡単な例を見て、 GopherJS を使ったプログラミングがどんなものになるかを説明します。
package main
import (
"github.com/gopherjs/gopherjs/js"
)
func main() {
js.Global.Get("document").Call("addEventListener", "click", func() {
println("clicked")
})
}
これは以下の JavaScript とほぼ等価です:
document.addEventListener('click', function() {
console.log('clicked');
})
上のコードおよび実行結果から、以下のことがわかります:
-
github.com/gopherjs/gopherjs/js
パッケージを通じて JavaScript の世界にアクセスできる -
js.Global
でグローバルオブジェクト (ブラウザの場合はwindow
) が取得できる-
js.Global
の型は*js.Object
である。 JavaScript の世界のすべての値はこの型になる (ただしnull
はnil
と等価)。詳しくは API ドキュメントを参照のこと。
-
-
*js.Object
のGet
で JavaScript のプロパティにアクセスできる。戻り値も*js.Object
である -
*js.Object
のCall
で JavaScript の関数呼び出しが出来る- ちなみに、
Get
で function オブジェクトを取得した後、Invoke
しても同じ効果が得られる
- ちなみに、
- コールバックとして Go の関数をそのまま渡すことが出来る
-
print
/println
はconsole.log
と同じく、コンソールに出力される- 日本語などの非 ASCII 文字は
print
/println
で文字化けするが、仕様なので注意すること。文字化けを避けるならばfmt.Println
などを使用する。fmt.Println
などの標準出力も同様にコンソールに出力されるが、これは Playground とは挙動が異なるので注意すること。
- 日本語などの非 ASCII 文字は
Go → JavaScript
Go の世界の値を JavaScript の世界の値に変換するのは割と暗黙的にやってくれます。例えば alert
関数の引数として、そのまま Go 文字列渡せます:
package main
import (
"github.com/gopherjs/gopherjs/js"
)
func main() {
js.Global.Call("alert", "Go の文字列")
}
同様に整数も渡せます:
package main
import (
"github.com/gopherjs/gopherjs/js"
)
func main() {
// JavaScript の Math.log2 を計算し、結果を alert で表示。
js.Global.Call("alert", js.Global.Get("Math").Call("log2", 4096))
}
js.MakeWrapper
を使うと、 Go のメソッド呼び出しを JavaScript 側でも呼び出すことができます:
package main
import (
"github.com/gopherjs/gopherjs/js"
)
type Foo struct {
message string
}
func (f *Foo) Say() {
js.Global.Call("alert", f.message)
}
func main() {
foo := &Foo{"Hello from Foo"}
js.Global.Set("fooInJS", js.MakeWrapper(foo))
// Go で定義された Say メソッドを JavaScript 側で呼び出し
js.Global.Get("fooInJS").Call("Say")
}
JavaScript → Go
JavaScript の世界の値はすべて *js.Object
型ですが、 String
や Int
などのメソッドを通じて Go の文字列や整数値に変換できます。
package main
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
)
func main() {
// String で JavaScript の文字列を Go の文字列に変換
ua := js.Global.Get("navigator").Get("userAgent").String()
fmt.Printf("UA: %s\n", ua)
}
文字列や整数などの基本的な型以外の変換は、 Interface
メソッドで行います。 Interface
で interface{}
型に変換でき、その後型アサーションで変換します。どのような型になるかは API ドキュメントを参照してください。
package main
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
)
func main() {
// object は map[string]interface{} に変換される
location := js.Global.Get("location").Interface().(map[string]interface{})
// location オブジェクトのプロパティのうち値が文字列のものだけを列挙
for key, value := range location {
if str, ok := value.(string); ok {
fmt.Printf("%s: %s\n", key, str)
}
}
}
サンプル
コールバックとチャネル
コールバックとチャネルを組み合わせると、「コールバックが呼ばれるまで待機」といった処理を自然に実現できます。 XHR でファイルを取得する際、実際にファイルを取得するまで (もしくはエラーになるまで) 待機する例は以下のとおりです:
// https://github.com/hajimehoshi/ebiten/blob/master/ebitenutil/file_js.go より一部改変
func OpenFile(path string) ([]uint8, error) {
var err error
var content *js.Object
ch := make(chan struct{})
req := js.Global.Get("XMLHttpRequest").New()
req.Call("open", "GET", path, true)
req.Set("responseType", "arraybuffer")
req.Call("addEventListener", "load", func() {
defer close(ch)
status := req.Get("status").Int()
if 200 <= status && status < 400 {
content = req.Get("response")
return
}
err = errors.New(fmt.Sprintf("http error: %d", status))
})
req.Call("addEventListener", "error", func() {
defer close(ch)
err = errors.New(fmt.Sprintf("XMLHttpRequest error: %s", req.Get("statusText").String()))
})
req.Call("send")
// load イベントまたは error イベント待機
<-ch
if err != nil {
return nil, err
}
data := js.Global.Get("Uint8Array").New(content).Interface().([]uint8)
return data, nil
}
受信後の処理が send のあとに連続で書けるわけです。
ブラウザゲーム
Go でゲームを書いて、ブラウザで動かすことができます。実際に作ったものはこちら。
JavaScript インタプリタ
Go で書かれた JavaScript エンジン Otto ですらも GopherJS で JavaScript に変換できます。なんとブラウザの上で JavaScript が実行できます。すごいですね!
おわりに
GopherJS に関する基本的な機能の概説を行いました。 GopherJS を使ってみようかな、と考えるきっかけになれば幸いです。