LoginSignup
100
89

More than 5 years have passed since last update.

go-bindata でコンパイル時にリソースを埋め込んじゃおう!

Last updated at Posted at 2014-07-10

概要

辞書データとかちょっとした画像ファイルとか,リソースを定数としてバイナリに持たせておきたいときってありますよね.たとえば,Java なら jar の中にちょっとしたリソースファイルとか含められるじゃないですか.go-bindata はそういった問題を解決するためのツールです.自分用に使い方をまとめたメモなので,違うよ!ってところがあったらご指摘いただけると嬉しいです :bow:

出典

下記の README に丁寧に書いてあるので,こちらを読むのが間違いないです.

go-bindata

仕組み

  • go-bindata は(バイナリ)データをプログラムに埋め込むためのツールです.
  • データをコードとして埋め込んで,それらにアクセスできるようなメソッドをつけた go のソースを生成してくれます.
  • データは gzip 圧縮してくれます (しないことも可能です)
  • 生成されたコードを加えてビルドすると,ビルドがとても遅くなってしまうことがありますが,これを回避する方法が提供されています.

インストール

成功すると GOBIN で指定されているフォルダに go-bindata という実行ファイルが出来ているはずです.

install
go get -u github.com/jteeuwen/go-bindata/...

使い方

フォルダを用意して埋め込みたいデータを入れておきます.ここでは data というフォルダとします.フォルダ内に含まれるファイルは基本的にはすべて対象になります(対象にしたくないファイルがある場合は ignore するオプションつける).

データファイルの変換
$ go-bindata data/

とすると,bindata.go というファイルが出来ます.このファイルの名前を変えたいときは -o オプションを使えば好きな名前に出来ます.

出来上がるファイルの中身をみてみる

data にファイルを2つ放り込んで試してみます.

データファイルの変換例
% tree data
data
├── goodby.txt
└── hello.txt

0 directories, 2 files
% go-bindata data
% ls
bindata.go data/
生成されたコード
package main

import (
    "bytes"
    "compress/gzip"
    "fmt"
    "io"
    "strings"
)

func bindata_read(data []byte, name string) ([]byte, error) {
    gz, err := gzip.NewReader(bytes.NewBuffer(data))
    if err != nil {
        return nil, fmt.Errorf("Read %q: %v", name, err)
    }

    var buf bytes.Buffer
    _, err = io.Copy(&buf, gz)
    gz.Close()

    if err != nil {
        return nil, fmt.Errorf("Read %q: %v", name, err)
    }

    return buf.Bytes(), nil
}

func data_goodby_txt() ([]byte, error) {
    return bindata_read([]byte{
        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x5a, 0xb2,
        0x75, 0xc9, 0x8b, 0x25, 0xcb, 0x96, 0x9c, 0x5a, 0xf2, 0x92, 0x0b, 0x10,
        0x00, 0x00, 0xff, 0xff, 0xd2, 0xb3, 0x6f, 0xfa, 0x0b, 0x00, 0x00, 0x00,
    },
        "data/goodby.txt",
    )
}

func data_hello_txt() ([]byte, error) {
    return bindata_read([]byte{
        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x5a, 0xb2,
        0x79, 0xc9, 0xe7, 0x25, 0xa7, 0x97, 0x1c, 0x5c, 0xf2, 0x9e, 0x0b, 0x10,
        0x00, 0x00, 0xff, 0xff, 0xeb, 0xd2, 0x91, 0xe3, 0x0b, 0x00, 0x00, 0x00,
    },
        "data/hello.txt",
    )
}

// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
    cannonicalName := strings.Replace(name, "\\", "/", -1)
    if f, ok := _bindata[cannonicalName]; ok {
        return f()
    }
    return nil, fmt.Errorf("Asset %s not found", name)
}

// AssetNames returns the names of the assets.
func AssetNames() []string {
    names := make([]string, 0, len(_bindata))
    for name := range _bindata {
        names = append(names, name)
    }
    return names
}

// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
    "data/goodby.txt": data_goodby_txt,
    "data/hello.txt": data_hello_txt,
}

データが埋め込まれている部分

こんな感じでデータがバイト列で埋め込まれています.hello.txt には こんにちわ.goodby.txt には さようなら と埋め込んだだけなのですが,ちょっと埋め込まれてるバイトは大きいですね・・・.(注:バイナリでも埋め込めるか分かりやすいように,テキストの文字コードは EUC にしました)

埋め込まれたデータ
func data_hello_txt() ([]byte, error) {
    return bindata_read([]byte{
        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x5a, 0xb2,
        0x79, 0xc9, 0xe7, 0x25, 0xa7, 0x97, 0x1c, 0x5c, 0xf2, 0x9e, 0x0b, 0x10,
        0x00, 0x00, 0xff, 0xff, 0xeb, 0xd2, 0x91, 0xe3, 0x0b, 0x00, 0x00, 0x00,
    },
        "data/hello.txt",
    )
}

元ファイルを od してみると

hexdump
0000000      b3a4    f3a4    cba4    c1a4    efa4    000a
0000013

うーん,全然大きいですね.と思ったのですが,変換時に勝手に gzip してくれてるのを忘れてました.-nocompress オプションをつけて生成してみると,

圧縮しない場合
func data_hello_txt() ([]byte, error) {
        return []byte{
                0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xef, 0x0a,
        }, nil
}

おお,たしかに埋め込まれたバイト列と一致します.(注:odコマンドはエンディアン依存で32bitリトルエンディアン環境)

ファイルはどうやって区別されてる?

1つのフォルダにファイルを放り込んだけれど,ファイルはどうやって区別されてるんでしょうか?
と思ったら,map で管理されてました.map の値は関数になってます.

内部でのファイルの管理
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
    "data/goodby.txt": data_goodby_txt,
    "data/hello.txt": data_hello_txt,
}

どうやってデータにアクセスする?

Asset という関数に名前を指定してやればいいようです.ここで指定する名前はファイルパス名です.ファイルパスに含まれる / とか ._ に置き換えた名前の関数を呼び出すみたいです.例えば,data/hello.txt なら,data_hello_text() が呼ばれます.これはデータが埋め込まれてた関数ですね!

データの呼び出し
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
    cannonicalName := strings.Replace(name, "\\", "/", -1)
    if f, ok := _bindata[cannonicalName]; ok {
        return f()
    }
    return nil, fmt.Errorf("Asset %s not found", name)
}

生成されるファイルのパッケージ名を変える

何も指定しないと,生成されるファイルのパッケージは main になります.パッケージ名は -pkg オプションで指定します.

サンプル

上記の hello.txt と goodby.txt を呼び出すサンプルを書いてみます.

サンプル
package main

import (
        "fmt"
)

func main() {
        hello, _ := Asset("data/hello.txt")
        fmt.Printf("%s\n", hello)
}

実行 (hello.txt は EUC)

実行結果
% go run bindata.go main.go|iconv -f EUC-JP -t UTF-8
こんにちわ

上手くいきました

コンパイルが遅いよ〜

埋め込むファイルが大きくなるとコンパイルが遅くなることがあります.これはバイト配列を返すようにすると,.dataセクションに埋め込もうとするため,コンパイルに時間がかかってしまう事が原因のようです.ではどうするか,埋め込むデータをテキストにすれば .rodataセクションに定数としてそのまま埋め込まれるはずなので,コンパイル時にはコストがかからない・・・というような感じで,埋め込むオプションがあります.-nomemcopy オプションをつけると,バイト列が文字列として埋め込まれます.その代わり,reflectunsafe のパッケージが使われるようになります.README から引用すると,下記のような感じになるそうです.

-nomemcopyなし
func myfile() []byte {
    return []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a}
}
-nomemcopyあり
var _myfile = "\x89\x50\x4e\x47\x0d\x0a\x1a"

func myfile() []byte {
    var empty [0]byte
    sx := (*reflect.StringHeader)(unsafe.Pointer(&_myfile))
    b := empty[:]
    bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    bx.Data = sx.Data
    bx.Len = len(_myfile)
    bx.Cap = bx.Len
    return b
}

あとがき

形態素解析に辞書を同梱しようとして,辞書構造を go のソースコードに変換してたんですが,(Pure Go な形態素解析器で実行バイナリに辞書埋め込んだヤツを作ってみた (1))コンパイルが非常に重くなるという問題に直面していました.go-bindata を使えば,ビルドもそれほどかからずデータを埋め込めそうです.

go-bindata では基本 gzip してくれるので,データがすでに圧縮してある場合とかはかえってデータが大きくなってしまいます.この辺は TODO に積まれてるみたいなので,今後改善していきそうです.

100
89
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
100
89