go-assets でシングルバイナリにまとめる

ここのところ小ネタばかりでゴメンペコン。

Go 言語の利点のひとつは単一の実行モジュール(バイナリ・ファイル)でアプリケーションを deploy できる点にある。 しかし,実行モジュール以外のファイル(たとえば固定の辞書ファイルやテンプレート・ファイルなど)がセットになっている場合はそうもいかなくなる。

そこで,実行モジュール以外の外部ファイルをソースコードに取り込んでマージすることにより,全部ひっくるめてシングルバイナリにする方法が考えられた。

ファイル内容を取り込めばその分バイナリのサイズが大きくなってしまうが,元々 Go 言語の実行モジュールはモノリシックな構造でファイルサイズが大きいので,組み込みなど計算リソースに制限がある場合を除けば,大した問題にはならないと思われる。

外部ファイルをマージする方法として,以下のパッケージが有名なようだ。

ただし jteeuwen/go-bindata のほうは awesome-go から削除されるなどちょっとアレな感じになってるみたいで,今後使うなら jessevdk/go-assets のほうがいいかもしれない。

というわけで,この記事では jessevdk/go-assets の使い方を簡単に紹介する。

まずはファイルを用意

今回のフォルダ・ファイル構成はこんな感じ。

hello/
│  hello.go
│
└─html/
        index.html

このうち html/index.html をマージしたい。 中身はこんな感じ。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello World!</title>
</head>
<body>
    <p>Hello World!</p>
</body>
</html>

go-assets-builder のインストール

index.html ファイルを Go 言語コードに変換するために jessevdk/go-assets-builder をインストールする。 バイナリは提供されていないので,ここは素直に go get コマンドを使う。

$ go get -v github.com/jessevdk/go-assets-builder

-h オプションで見るとこんな感じ。

$ go-assets-builder -h
Usage:
  go-assets-builder.exe [OPTIONS] FILES...

Application Options:
  -p, -package:       The package name to generate the assets for (default: main)
  -v, -variable:      The name of the generated asset tree (default: Assets)
  -s, -strip-prefix:  Strip the specified prefix from all paths
  -o, -output:        File to write output to, or - to write to stdout (default: -)

Help Options:
  -?                  Show this help message
  -h, -help           Show this help message

ソース・コードの生成

早速 hello/ フォルダ直下で go-assets-builder を動かしてみる。

$  go-assets-builder html/ > assets.go

これで assets.go ファイルが生成された。 中身はこんな感じ。

package main

import (
    "time"

    "github.com/jessevdk/go-assets"
)

var _Assets950a25363eee220f7d8ce234bcc0b349e4ea9072 = "<!DOCTYPE html>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<title>Hello World!</title>\n</head>\n<body>\n\t<p>Hello World!</p>\n</body>\n</html>"

// Assets returns go-assets FileSystem
var Assets = assets.NewFileSystem(map[string][]string{"/html": []string{"index.html"}, "/": []string{"html"}}, map[string]*assets.File{
    "/": &assets.File{
        Path:     "/",
        FileMode: 0x800001ff,
        Mtime:    time.Unix(1518523695, 1518523695933225200),
        Data:     nil,
    }, "/html": &assets.File{
        Path:     "/html",
        FileMode: 0x800001ff,
        Mtime:    time.Unix(1518522609, 1518522609437368800),
        Data:     nil,
    }, "/html/index.html": &assets.File{
        Path:     "/html/index.html",
        FileMode: 0x1b6,
        Mtime:    time.Unix(1518523737, 1518523737979915400),
        Data:     []byte(_Assets950a25363eee220f7d8ce234bcc0b349e4ea9072),
    }}, "")

パッケージ名(既定は main)は -p オプションで,変数名(既定は Assets)は -v オプションで変更できる。

上の例では index.html ファイルのパスは実際のフォルダ構成のまま /html/index.html となっているが, html/ フォルダ直下をドキュメント・ルートにするのであれば -s="/html" とする。

$ go-assets-builder -p data -v Docs -s="/html" html
package data

import (
    "time"

    "github.com/jessevdk/go-assets"
)

var _Docs950a25363eee220f7d8ce234bcc0b349e4ea9072 = "<!DOCTYPE html>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<title>Hello World!</title>\n</head>\n<body>\n\t<p>Hello World!</p>\n</body>\n</html>"

// Docs returns go-assets FileSystem
var Docs = assets.NewFileSystem(map[string][]string{"/": []string{"index.html"}}, map[string]*assets.File{
    "/": &assets.File{
        Path:     "/",
        FileMode: 0x800001ff,
        Mtime:    time.Unix(1518522609, 1518522609437368800),
        Data:     nil,
    }, "/index.html": &assets.File{
        Path:     "/index.html",
        FileMode: 0x1b6,
        Mtime:    time.Unix(1518523737, 1518523737979915400),
        Data:     []byte(_Docs950a25363eee220f7d8ce234bcc0b349e4ea9072),
    }}, "")

組み込んだファイルを読む

生成したソース・コードで定義した Assets 変数を使って hello.go ファイルの main() 関数は以下のように書ける。

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    f, err := Assets.Open("/html/index.html")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    io.Copy(os.Stdout, f)
}

では,実行してみよう。 こんな感じになる。

$ go run hello.go assets.go
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello World!</title>
</head>
<body>
    <p>Hello World!</p>
</body>
</html>

まぁ,テキストを表示してるだけだけど。

ちなみに assets.FileSystemhttp.FileSystem インタフェースと互換がある。 以下は http.FileSystem インタフェースの定義。

type FileSystem interface {
    Open(name string) (File, error)
}

従って assets.FileSystem を使って以下のような簡易 Web サーバも組める。

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    fmt.Println("Open http://localhost:3000/html/")
    fmt.Println("Press ctrl+c to stop")

    http.Handle("/", http.FileServer(Assets))
    if err := http.ListenAndServe(":3000", nil); err != nil {
        fmt.Fprintln(os.Stderr, err)
    }
}

go generate コマンドによるソース・コードの生成

// コメントに go:generate マーカを使うことにより go generate コマンドで go-assets-builder コマンドを呼び出せる。

 package main

 import (
     "fmt"
     "net/http"
     "os"
 )

 //go:generate go-assets-builder -s="/html" -o assets.go html/

 func main() {
     fmt.Println("Open http://localhost:3000/")
     fmt.Println("Press ctrl+c to stop")

     http.Handle("/", http.FileServer(Assets))
     if err := http.ListenAndServe(":3000", nil); err != nil {
         fmt.Fprintln(os.Stderr, err)
     }
 }
 
$ go generate

$ go run hello.go assets.go
Open http://localhost:3000/
Press ctrl+c to stop

go generate コマンドは明示的に行う必要があるので注意。 ビルドでソース・コード生成を含めて自動化したいなら make とか導入する必要があるかも。

ブックマーク

参考図書

photo
みんなのGo言語【現場で使える実践テクニック】
松木雅幸 mattn 藤原俊一郎 中島大一 牧 大輔 鈴木健太 稲葉貴洋
技術評論社 2016-09-09
評価

WEB+DB PRESS Vol.95 プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES) プログラミングElixir Go言語によるWebアプリケーション開発 詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE) WEB+DB PRESS Vol.94 Docker スターティングGo言語 (CodeZine BOOKS) オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 WebデベロッパーのためのReact開発入門 JavaScript UIライブラリの基本と活用

リファレンス本なのに索引が貧弱。これなら Kindle で買ってもよかったか。 1.7 への言及があるのはよかった。

reviewed by Spiegel on 2016-11-17 (powered by G-Tools)