GolangのGin/bindataでシングルバイナリを試してみた(+React)

# はじめに

Go言語と言えばシングルバイナリが魅力の一つですが、Webサーバとして利用する際のHTML/JS/CSSといったリソースは自動的には埋め込められません。やりたければ別途、[go-bindata](https://github.com/jteeuwen/go-bindata)などのツールを使って実行ファイルに埋め込む必要があります。今回、Go言語の[Gin](https://github.com/gin-gonic/gin)を使ったWebアプリのシングルバイナリ化を試してみたので簡単に紹介する。

なお、埋め込むリソースとなるクライアント側は[Facebook React](https://facebook.github.io/react/)を使ったアプリになっています(お試しなのでHello Worldと表示するだけですが...)。Reactのサーバサイドレンダリングは今回は試していませんが、[go-duktape](https://github.com/olebedev/go-duktape)を使えばGo言語でもできそうなので別途試してみたいと思っています。

# 作ったもの

https://github.com/wadahiro/gin-react-boilerplate にあります。

# ディレクトリ構成

下記のようなレイアウトとした。

* `assets`: 最終的にGo言語の実行ファイルに埋め込むリソース群。`go-bindata`を使ってここからGo言語のソースを生成する。
* `client`: Reactを使ったクライアントアプリ。[webpack](http://webpack.github.io/)でビルドし、`bundle.js`を生成する。`bundle.js`は`assets/js`に出力する。
* `server`: Go言語で書いたサーバアプリ。Ginを利用。
* `dist`: リリースビルドをするとここに実行ファイルが生成される。

```
gin-react-boilerplate
├─assets
│  ├─css
│  │  └─main.css
│  ├─index.html
│  └─js
│      └─bundle.js
├─client
│  ├─app
│  │  └─App.jsx
│  ├─index.jsx
│  └─webpack
│      ├─webpack.base.config.js
│      ├─webpack.config.js
│      ├─webpack.dev.config.js
│      ├─webpack.prod.config.js
│      └─webpack.server.js
├─dist
├─.gitignore
├─package.json
├─runner.conf
└─server
   ├─controllers
   │  └─home.go
   └─main.go
```

### 補足
* `runner.conf`: [Fresh](https://github.com/pilu/fresh)の設定ファイル。FreshはGo言語のソース修正時に自動的にコンパイル&Webサーバを再起動してくれる。開発時にはこれを使う。
* `package.json`: クライアントアプリのビルドに必要な各種ライブラリの依存関係定義や、`npm run ...`で[実行可能なタスクも少し定義](https://github.com/wadahiro/gin-react-boilerplate/blob/master/package.json#L9-L12)している。
* `.gitignore`: ビルドで生成されるものはソースリポジトリにコミットしない方針とし、`dist`、`bundle.js`、`bindata.go`は除外対象にしている。

# クライアント
最終的に`assets`に埋め込みたいリソースを置くようにすれば良い。`client`以下にあるReactを使ったアプリケーションについては、今回はwebpackを使っているので`webpack`コマンドでビルドして`bundle.js`を`assets/js`に生成するだけ。特に注意点はなし。

# サーバ
gin + bindataのやり方を調べたのでそこをメインに紹介。

最初に必要なライブラリをインストール。Webサーバのリソースを扱うため、`http.FileSystem`インタフェースを実装したソースを生成してくれる[go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs)もインストールする。

```bash
go get -u github.com/gin-gonic/gin
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/elazarl/go-bindata-assetfs/...
```

埋め込むリソースフォルダを指定して`go-bindata-assetfs`コマンドを実行する。これで`bindata_assetfs.go`が生成される。また、`-o`オプションで生成先を指定できる。

```bash
go-bindata-assetfs -o ./server/bindata_assetfs.go assets/...
```

生成された`bindata_assetfs.go`を使い下記のようなコードを書くことで、埋め込んだリソースをHTTPで返すようにできる。Ginの[StaticFSメソッド](https://godoc.org/github.com/gin-gonic/gin#RouterGroup.StaticFS)と`bindata_assetfs.go`の`assetFS()`を組み合わせるだけと簡単。

```go:main.go
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.StaticFS("/assets", assetFS())

	r.Run()
}
```

生成した`bindata_assetfs.go`と`main.go`を`go run`でビルド&実行する。

```bash
$ go run server/bindata_asssetfs.go server/main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /assets/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] HEAD   /assets/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

curlで叩くと埋め込んだ`index.html`がちゃんと返ってくることを確認できる。

```bash
$ curl http://localhost:8080/assets/
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title></title>
        <link rel="stylesheet" href="./css/main.css">
    </head>
    <body>
        <div id="app"></div>

        <script src="js/bundle.js"></script>
    </body>
</html>
```

簡単ですね！！ と言いたいところですが、静的リソースを`/`でアクセスできるようにしたい場合に残念ながら困ります。例えば、以下のように`/`に静的リソース、`/api/home`にJSONを返す処理を実装したとすると、、、

```go:main.go
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.StaticFS("/", assetFS())

	r.GET("/api/home", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "home",
		})
	})

	r.Run()
}
```

コンパイルは通るけど実行時に下記のようなconflictsエラーになってしまう。

```bash
$ go run server/bindata_assetfs.go server/main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /*filepath                --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] HEAD   /*filepath                --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] GET    /api/home                 --> main.main.func1 (3 handlers)
panic: path segment '/api/home' conflicts with existing wildcard '/*filepath' in path '/api/home'
...
```

GinのIssueを漁ると [Inability to use '/' for static files](https://github.com/gin-gonic/gin/issues/75) を発見。ここを見ると、[static middleware](https://github.com/gin-gonic/contrib/tree/master/static)を使えば良さそう。

というわけでstatic middlewareをインストール。

```bash
go get github.com/gin-gonic/contrib/static
```

[static middleware + go-bindataのサンプル](https://github.com/gin-gonic/contrib/blob/master/static/example/bindata/example.go)を参考にして、`main.go`を書き換える。残念ながらインタフェースを合わせるためにちょっと複雑になってしまう。なお、[static middleware + go-bindataのサンプル](https://github.com/gin-gonic/contrib/blob/master/static/example/bindata/example.go)そのままではコンパイルが通らないので注意。`assetfs.AssetFS`に`AssetInfo`も渡さないと駄目です。

```go:main.go
package main

import (
	"net/http"
	"strings"

	"github.com/elazarl/go-bindata-assetfs"
	"github.com/gin-gonic/contrib/static"
	"github.com/gin-gonic/gin"
)

type binaryFileSystem struct {
	fs http.FileSystem
}

func (b *binaryFileSystem) Open(name string) (http.File, error) {
	return b.fs.Open(name)
}

func (b *binaryFileSystem) Exists(prefix string, filepath string) bool {
	if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
		if _, err := b.fs.Open(p); err != nil {
			return false
		}
		return true
	}
	return false
}

func BinaryFileSystem(root string) *binaryFileSystem {
	fs := &assetfs.AssetFS{Asset, AssetDir, AssetInfo, root}
	return &binaryFileSystem{
		fs,
	}
}

func main() {
	r := gin.Default()

	r.Use(static.Serve("/", BinaryFileSystem("assets")))

	r.GET("/api/home", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello World",
		})
	})

	r.Run()
}
```

また、bindataは`go-bindata`を使って生成するように変更する。

```bash
go-bindata -o ./server/bindata.go assets/...
```

これで`/`に`assets`配下の静的リソースを割り当てつつ、`/api/home`の処理も実装できるようになる。`go run`すると今度は起動成功する(`bindata_assetfs.go`はもう不要なので消しておく)。

```bash
$ go run server/bindata.go server/main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/home                 --> main.main.func1 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

curlで`/`にアクセスすると埋め込んだindex.htmlがちゃんと返ってくることを確認できる。

```bash
$ curl http://localhost:8080/
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title></title>
        <link rel="stylesheet" href="./css/main.css">
    </head>
    <body>
        <div id="app"></div>

        <script src="js/bundle.js"></script>
    </body>
</html>
```

Go言語で実装したREST APIもOK。

```bash
$ curl http://localhost:8080/api/home
{"message":"Hello World"}
```


# 開発モードで効率化

## クライアントアプリの開発モード
`client`以下のReactで作っている部分に関しては、`webpack`コマンドをwatchモードで起動しておくことでソースの変更時に自動的にビルドして`assets/js/bundle.js`に反映させることができる。`npm run dev`で実行できるように`package.json`にタスクを設定しているのでこれを叩いてビルドする。

```bash
$ npm run dev

> golang-web-boilerplate@0.1.0 dev c:\Users\wadahiro\src\github.com\wadahiro\gin-react-boilerplate
> webpack -w --config ./client/webpack/webpack.config.js

NODE_ENV: undefined
Hash: a307cf2e4d362646c0a4
Version: webpack 1.12.12
Time: 1867ms
    Asset    Size  Chunks             Chunk Names
bundle.js  851 kB       0  [emitted]  main
   [0] multi main 28 bytes {0} [built]
    + 350 hidden modules
```

これで`client`以下のソースに手を入れた際はwebpackが自動的に`assets/js/bundle.js`をビルドしてくれる。

## bindataの生成
`go-bindata -o ./server/bindata.go assets/...`で生成した`bindata.go`は、クライアント側のソース修正のたびに`go-bindata`の再実行が必要になる。これを回避するための機能が`go-bindata`にある。使い方は簡単で、`-debug`オプションをつけて実行してソースを生成するだけ。

```bash
go-bindata -debug -o ./server/bindata.go assets/...
```

そうすると、生成される`bindata.go`にはリソースは直接埋め込まれず、`assets`以下のオリジナルのファイルを都度読むような実装となる。開発時はこのソースを組み込んでWebサーバを起動すればOK。これでクライアント側のリソースファイルが変更されてもすぐに反映される。

## サーバアプリの開発モード
冒頭で少し紹介したFreshを使う。先程は`go run`でWebアプリを起動していたが、代わりに`fresh`コマンドを使う。`runner.conf`に適切に設定していれば、下記のように`fresh`コマンドだけで起動する。

```bash
$ fresh
Loading settings from ./runner.conf
13:21:19 runner      | InitFolders
13:21:19 runner      | mkdir ./tmp
13:21:19 runner      | mkdir ./tmp: Cannot create a file when that file already exists.
13:21:19 watcher     | Watching ./server
13:21:19 watcher     | Watching server\controllers
13:21:19 main        | Waiting (loop 1)...
13:21:19 main        | receiving first event /
13:21:19 main        | sleeping for 600 milliseconds
13:21:19 main        | flushing events
13:21:19 main        | Started! (12 Goroutines)
13:21:19 main        | remove tmp\runner-build-errors.log: The system cannot find the file specified.
13:21:19 build       | Building...
13:21:22 runner      | Running...
13:21:22 main        | --------------------
13:21:22 main        | Waiting (loop 2)...
13:21:22 app         | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

13:21:22 app         | [GIN-debug] GET    /api/home                 --> main.main.func1 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

試しにGo言語のソースを修正すると下記のように自動的に検知して再起動が行われる。

```bash
13:22:08 watcher     | sending event "server\\controllers\\home.go": MODIFY
13:22:08 watcher     | sending event "server\\controllers\\home.go": MODIFY
13:22:08 main        | receiving first event "server\\controllers\\home.go": MODIFY
13:22:08 main        | sleeping for 600 milliseconds
13:22:09 main        | flushing events
13:22:09 main        | receiving event "server\\controllers\\home.go": MODIFY
13:22:09 main        | Started! (15 Goroutines)
13:22:09 main        | remove tmp\runner-build-errors.log: The system cannot find the file specified.
13:22:09 build       | Building...
13:22:11 runner      | Running...
13:22:11 runner      | Killing PID 7532
13:22:12 main        | --------------------
13:22:12 main        | Waiting (loop 3)...
13:22:12 app         | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

13:22:12 app         | [GIN-debug] GET    /api/home                 --> main.main.func1 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

クライアントとサーバ側の開発モードをまとめて実行したければ、下記コマンドを実行すれば良い。

```bash
npm run dev & fresh
```

# リリースビルド

リリースビルドの時には、今度は`go-bindata`を`-debug`オプションなしで実行してソースを生成し、それを組み込んでビルドすれば良い。フルビルドの手順としては、

1. `webpack -p`でクライアントアプリをリリース用にビルドして`assets/js/bundle.js`を生成
2. `go-bindata`で`assets`以下のファイルから`bindata.go`を生成
3. `go build`でビルドして実行ファイルを`dist`に生成

という流れになる。ワンライナーで書くと以下。

```bash
NODE_ENV=production && webpack -p --config ./client/webpack/webpack.config.js && go-bindata -o ./server/bindata.go assets/... && go build -o ./dist/web server/bindata.go server/main.go
```

※Windowsだと、`set NODE_ENV=production`にする必要あり。

これを`npm run build`で実行できるように`package.json`に設定しているので、

```bash
npm run build
```

と実行すれば、実行可能なシングルバイナリとして`./dist/web` が生成される。ちなみに今回作ったお試しアプリのサイズは10MBほどでした。

# シングルバイナリで動かしてみる

`dist`以下にできた実行ファイルを起動するだけ。このファイル1つをデプロイするだけで良いので配布&インストールが超楽ちんですね！

```bash
$ ./dist/web
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/home                 --> main.main.func1 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

# まとめ
* ちょっと面倒なところはあったが、Gin + bindataでシングルバイナリを作ることができた
* watchモードがクライアント、サーバサイドの両方にあるので、高速フィードバックで開発できそう
* せっかくReactを使っているので、サーバサイドレンダリングを今度は試してみます。