GopherJS の基礎

本稿では、筆者が普段使っているツールの一つである、 GopherJS の基本的な使い方について説明します。

[GopherJS](http://www.gopherjs.org/) は [Go](https://golang.org/) で書いたプログラムを JavaScript に変換するソフトウェアです。生成した JavaScript は、 Web ブラウザはもちろん、 [node.js](https://nodejs.org/) で実行することが出来ます。 Web アプリケーションを書かなければならないが、どうしても Go で書きたいという、 Go プログラマーによくある (?) お悩みにうってつけです。

変換結果はそこそこ素直な読みやすい JavaScript になります。 asm.js に変換する [Emscripten](https://github.com/kripken/emscripten) や、バイナリ形式の [Web Assembly](http://webassembly.org/) とは異なるアプローチです。 GopherJS のアプローチの場合、デバッグやプロファイルはしやすいですが、ファイルサイズやパフォーマンスには若干難があります。

なお本稿では node.js よりはブラウザ用途を中心に解説します。

# 使い方

GopherJS のコマンド `gopherjs` は `go get` でインストールできます。

```shell-session
go get -u github.com/gopherjs/gopherjs/...
```

ビルドしたいパッケージを `gopherjs` コマンドをパッケージを指定して実行するだけです。

```shell-session
gopherjs build github.com/yourname/yourproject
```

main パッケージならば、 js ファイルと map ファイルがその場に生成されます。 `build` 以外にもサブコマンド `gopherjs install` や `gopherjs get` などがあり、 `go` コマンドのそれと似ています。詳しくは `gopherjs help` を参照してください。

インストールせずに試したい場合は、 [GopherJS Playground](https://gopherjs.github.io/playground/) で実験することが出来ます。変換結果は得られませんが、ほとんどすべての Go プログラムを GopherJS によってブラウザで実行できることが確認できると思います。

# 機能

## 出来ること

- Go の言語機能すべて
- [ほぼすべての標準ライブラリ](https://github.com/gopherjs/gopherjs/blob/master/doc/packages.md)。 [os](https://golang.org/pkg/os/) パッケージなどはブラウザではなく node で使われることを想定しています。
- 任意の JavaScript 関数の呼び出し
- 数値、文字列、バイト配列などの一部の型の相互変換

## 出来ないこと

- [cgo](https://golang.org/cmd/cgo/)
- [unsafe](https://golang.org/pkg/unsafe/) など、一部の標準ライブラリ。


# 簡単な例

実際に簡単な例を見て、 GopherJS を使ったプログラミングがどんなものになるかを説明します。

```go
package main

import (
	"github.com/gopherjs/gopherjs/js"
)

func main() {
	js.Global.Get("document").Call("addEventListener", "click", func() {
		println("clicked")
	})
}
```

これは以下の JavaScript とほぼ等価です:

```js
document.addEventListener('click', function() {
    console.log('clicked');
})
```

上のコードおよび実行結果から、以下のことがわかります:

- `github.com/gopherjs/gopherjs/js` パッケージを通じて JavaScript の世界にアクセスできる
- `js.Global` でグローバルオブジェクト (ブラウザの場合は `window`) が取得できる
  - `js.Global` の型は `*js.Object` である。 JavaScript の世界のすべての値はこの型になる (ただし `null` は `nil` と等価)。詳しくは [API ドキュメント](https://godoc.org/github.com/gopherjs/gopherjs/js
)を参照のこと。
- `*js.Object` の `Get` で JavaScript のプロパティにアクセスできる。戻り値も `*js.Object` である
- `*js.Object` の `Call` で JavaScript の関数呼び出しが出来る
  - ちなみに、 `Get` で function オブジェクトを取得した後、 `Invoke` しても同じ効果が得られる
- コールバックとして Go の関数をそのまま渡すことが出来る
- `print`/`println` は `console.log` と同じく、コンソールに出力される
  - 日本語などの非 ASCII 文字は `print`/`println` で文字化けするが、[仕様](https://github.com/gopherjs/gopherjs/issues/44)なので注意すること。文字化けを避けるならば `fmt.Println` などを使用する。 `fmt.Println` などの標準出力も同様にコンソールに出力されるが、これは [Playground](http://www.gopherjs.org/play/) とは挙動が異なるので注意すること。

# Go → JavaScript

Go の世界の値を JavaScript の世界の値に変換するのは割と暗黙的にやってくれます。例えば `alert` 関数の引数として、そのまま Go 文字列渡せます:

```go
package main

import (
	"github.com/gopherjs/gopherjs/js"
)

func main() {
	js.Global.Call("alert", "Go の文字列")
}
```

同様に整数も渡せます:

```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 側でも呼び出すことができます:

```go
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 の文字列や整数値に変換できます。

```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 ドキュメント](https://godoc.org/github.com/gopherjs/gopherjs/js)を参照してください。

```go
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 でファイルを取得する際、実際にファイルを取得するまで (もしくはエラーになるまで) 待機する例は以下のとおりです:

```go
// 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 でゲームを書いて、ブラウザで動かすことができます。実際に作ったものはこちら。

* [いの べーしょん 2007](http://hajimehoshi.github.io/go-inovation/) ([ソースコード](https://github.com/hajimehoshi/go-inovation))

<img width="640" alt="ino.png" src="https://qiita-image-store.s3.amazonaws.com/0/1911/449f3703-fb5a-5b5d-aa24-984bd36ed474.png">


## JavaScript インタプリタ

Go で書かれた JavaScript エンジン [Otto](https://github.com/robertkrimen/otto) ですらも GopherJS で JavaScript に変換できます。なんとブラウザの上で JavaScript が実行できます。すごいですね!

- https://github.com/hajimehoshi/otto-gopherjs

# おわりに

GopherJS に関する基本的な機能の概説を行いました。 GopherJS を使ってみようかな、と考えるきっかけになれば幸いです。