はじめに
Go言語と言えばシングルバイナリが魅力の一つですが、Webサーバとして利用する際のHTML/JS/CSSといったリソースは自動的には埋め込められません。やりたければ別途、go-bindataなどのツールを使って実行ファイルに埋め込む必要があります。今回、Go言語のGinを使ったWebアプリのシングルバイナリ化を試してみたので簡単に紹介する。
なお、埋め込むリソースとなるクライアント側はFacebook Reactを使ったアプリになっています(お試しなのでHello Worldと表示するだけですが...)。Reactのサーバサイドレンダリングは今回は試していませんが、go-duktapeを使えばGo言語でもできそうなので別途試してみたいと思っています。
作ったもの
https://github.com/wadahiro/gin-react-boilerplate にあります。
ディレクトリ構成
下記のようなレイアウトとした。
-
assets
: 最終的にGo言語の実行ファイルに埋め込むリソース群。go-bindata
を使ってここからGo言語のソースを生成する。 -
client
: Reactを使ったクライアントアプリ。webpackでビルドし、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の設定ファイル。FreshはGo言語のソース修正時に自動的にコンパイル&Webサーバを再起動してくれる。開発時にはこれを使う。 -
package.json
: クライアントアプリのビルドに必要な各種ライブラリの依存関係定義や、npm run ...
で実行可能なタスクも少し定義している。 -
.gitignore
: ビルドで生成されるものはソースリポジトリにコミットしない方針とし、dist
、bundle.js
、bindata.go
は除外対象にしている。
クライアント
最終的にassets
に埋め込みたいリソースを置くようにすれば良い。client
以下にあるReactを使ったアプリケーションについては、今回はwebpackを使っているのでwebpack
コマンドでビルドしてbundle.js
をassets/js
に生成するだけ。特に注意点はなし。
サーバ
gin + bindataのやり方を調べたのでそこをメインに紹介。
最初に必要なライブラリをインストール。Webサーバのリソースを扱うため、http.FileSystem
インタフェースを実装したソースを生成してくれるgo-bindata-assetfsもインストールする。
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
オプションで生成先を指定できる。
go-bindata-assetfs -o ./server/bindata_assetfs.go assets/...
生成されたbindata_assetfs.go
を使い下記のようなコードを書くことで、埋め込んだリソースをHTTPで返すようにできる。GinのStaticFSメソッドとbindata_assetfs.go
のassetFS()
を組み合わせるだけと簡単。
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
でビルド&実行する。
$ 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
がちゃんと返ってくることを確認できる。
$ 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を返す処理を実装したとすると、、、
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エラーになってしまう。
$ 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 を発見。ここを見ると、static middlewareを使えば良さそう。
というわけでstatic middlewareをインストール。
go get github.com/gin-gonic/contrib/static
static middleware + go-bindataのサンプルを参考にして、main.go
を書き換える。残念ながらインタフェースを合わせるためにちょっと複雑になってしまう。なお、static middleware + go-bindataのサンプルそのままではコンパイルが通らないので注意。assetfs.AssetFS
にAssetInfo
も渡さないと駄目です。
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
を使って生成するように変更する。
go-bindata -o ./server/bindata.go assets/...
これで/
にassets
配下の静的リソースを割り当てつつ、/api/home
の処理も実装できるようになる。go run
すると今度は起動成功する(bindata_assetfs.go
はもう不要なので消しておく)。
$ 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がちゃんと返ってくることを確認できる。
$ 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。
$ curl http://localhost:8080/api/home
{"message":"Hello World"}
開発モードで効率化
クライアントアプリの開発モード
client
以下のReactで作っている部分に関しては、webpack
コマンドをwatchモードで起動しておくことでソースの変更時に自動的にビルドしてassets/js/bundle.js
に反映させることができる。npm run dev
で実行できるようにpackage.json
にタスクを設定しているのでこれを叩いてビルドする。
$ 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
オプションをつけて実行してソースを生成するだけ。
go-bindata -debug -o ./server/bindata.go assets/...
そうすると、生成されるbindata.go
にはリソースは直接埋め込まれず、assets
以下のオリジナルのファイルを都度読むような実装となる。開発時はこのソースを組み込んでWebサーバを起動すればOK。これでクライアント側のリソースファイルが変更されてもすぐに反映される。
サーバアプリの開発モード
冒頭で少し紹介したFreshを使う。先程はgo run
でWebアプリを起動していたが、代わりにfresh
コマンドを使う。runner.conf
に適切に設定していれば、下記のようにfresh
コマンドだけで起動する。
$ 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言語のソースを修正すると下記のように自動的に検知して再起動が行われる。
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
クライアントとサーバ側の開発モードをまとめて実行したければ、下記コマンドを実行すれば良い。
npm run dev & fresh
リリースビルド
リリースビルドの時には、今度はgo-bindata
を-debug
オプションなしで実行してソースを生成し、それを組み込んでビルドすれば良い。フルビルドの手順としては、
-
webpack -p
でクライアントアプリをリリース用にビルドしてassets/js/bundle.js
を生成 -
go-bindata
でassets
以下のファイルからbindata.go
を生成 -
go build
でビルドして実行ファイルをdist
に生成
という流れになる。ワンライナーで書くと以下。
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
に設定しているので、
npm run build
と実行すれば、実行可能なシングルバイナリとして./dist/web
が生成される。ちなみに今回作ったお試しアプリのサイズは10MBほどでした。
シングルバイナリで動かしてみる
dist
以下にできた実行ファイルを起動するだけ。このファイル1つをデプロイするだけで良いので配布&インストールが超楽ちんですね!
$ ./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を使っているので、サーバサイドレンダリングを今度は試してみます。