GopherJS の基礎

  • 54
    Like
  • 0
    Comment

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

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

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

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

使い方

GopherJS のコマンド gopherjsgo get でインストールできます。

go get -u github.com/gopherjs/gopherjs/...

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

gopherjs build github.com/yourname/yourproject

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

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

機能

出来ること

  • Go の言語機能すべて
  • ほぼすべての標準ライブラリos パッケージなどはブラウザではなく node で使われることを想定しています。
  • 任意の JavaScript 関数の呼び出し
  • 数値、文字列、バイト配列などの一部の型の相互変換

出来ないこと

  • cgo
  • unsafe など、一部の標準ライブラリ。

簡単な例

実際に簡単な例を見て、 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 の世界のすべての値はこの型になる (ただし nullnil と等価)。詳しくは API ドキュメントを参照のこと。
  • *js.ObjectGet で JavaScript のプロパティにアクセスできる。戻り値も *js.Object である
  • *js.ObjectCall で JavaScript の関数呼び出しが出来る
    • ちなみに、 Get で function オブジェクトを取得した後、 Invoke しても同じ効果が得られる
  • コールバックとして Go の関数をそのまま渡すことが出来る
  • print/printlnconsole.log と同じく、コンソールに出力される
    • 日本語などの非 ASCII 文字は print/println で文字化けするが、仕様なので注意すること。文字化けを避けるならば fmt.Println などを使用する。 fmt.Println などの標準出力も同様にコンソールに出力されるが、これは Playground とは挙動が異なるので注意すること。

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 型ですが、 StringInt などのメソッドを通じて 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 メソッドで行います。 Interfaceinterface{} 型に変換でき、その後型アサーションで変換します。どのような型になるかは 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 でゲームを書いて、ブラウザで動かすことができます。実際に作ったものはこちら。

ino.png

JavaScript インタプリタ

Go で書かれた JavaScript エンジン Otto ですらも GopherJS で JavaScript に変換できます。なんとブラウザの上で JavaScript が実行できます。すごいですね!

おわりに

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