概要
辞書データとかちょっとした画像ファイルとか,リソースを定数としてバイナリに持たせておきたいときってありますよね.たとえば,Java なら jar の中にちょっとしたリソースファイルとか含められるじゃないですか.go-bindata
はそういった問題を解決するためのツールです.自分用に使い方をまとめたメモなので,違うよ!ってところがあったらご指摘いただけると嬉しいです
出典
下記の README に丁寧に書いてあるので,こちらを読むのが間違いないです.
go-bindata
仕組み
- go-bindata は(バイナリ)データをプログラムに埋め込むためのツールです.
- データをコードとして埋め込んで,それらにアクセスできるようなメソッドをつけた go のソースを生成してくれます.
- データは gzip 圧縮してくれます (しないことも可能です)
- 生成されたコードを加えてビルドすると,ビルドがとても遅くなってしまうことがありますが,これを回避する方法が提供されています.
インストール
成功すると GOBIN
で指定されているフォルダに go-bindata
という実行ファイルが出来ているはずです.
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
してみると
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
オプションをつけると,バイト列が文字列として埋め込まれます.その代わり,reflect
と unsafe
のパッケージが使われるようになります.README から引用すると,下記のような感じになるそうです.
func myfile() []byte {
return []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a}
}
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 に積まれてるみたいなので,今後改善していきそうです.