Goでファイル系のリソースも一緒にビルドして配布しちゃう!

例えば下記のようなディレクトリ構成のGoプロジェクトがあったとする。

.
├── files
│   └── sample.txt
└── main.go

sampleの中身は

sample!
sample!!
sample!!!

main.goの中身は

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("files/sample.txt")
    if err != nil {
        panic(err)
    }
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    f.Close()
}

実行するとfiles/sample.txtの中身がコンソールに出力される。単純なもの。

$ go run main.go
sample!
sample!!
sample!!!

例えばこれをbuildしてGithubのReleaseとかで配布することを考える。

$ go build -o /tmp/print_file  main.go

/tmp/print_fileでビルドされた成果物が置かれるので、これを実行する。

$ /tmp/print_file                                                                                                                                                                                                               
panic: open files/sample.txt: no such file or directory

go buildでは指定されたパッケージの依存関係を解決していきながらビルドしていくのでGoで書かれていれば含まれるのですが、今回のファイルのようなリソースは含まれないため起きたようです。

flagパッケージを使ってコマンドラインからフィアルパスを渡してあげるとかにすると思うのですが、GithubのReleasesとかでバイナリファイルして置くだけで実行したい時ってあると思うんですよ!

go-bindata

github.com

go-bindataは指定されたリソースをバイト列でGoのソースコードに記載してそのデータにアクセスできるメソッドをつけてくれます。
例えば今回はfiles配下のファイルを対象としたいので下記のようにコマンドを実行します。

$ go-bindata files

このコマンドを実行することでbindata.goが生成されます。

.
├── bindata.go
├── files
│   └── sample.txt
└── main.go

今生成したデータに対してアクセスするにはREADME.mdにも記載されていますがAsset を使用します。
今回のファイルの中身を出力するものを書き換えると

package main

import (
    "fmt"
)

func main() {
    b, err := Asset("files/sample.txt")
    if err != nil {
        panic(err)
    }
    s := string(b)
    fmt.Print(s)
}

Assetでは[]byteで返ってきます。
先程と同じようにビルドします。今回は先程go-bindataで作成したbindata.goも一緒にビルドに含めます。

$ go build -o /tmp/print_file main.go bindata.go
$ /tmp/print_file
sample!
sample!!
sample!!!

おー

最後に

今回はGoのビルドを行う際にGoファイル以外のリソースを含める方法を書いた。
大きすぎるファイルはビルドにも時間がかかるし配布も大変だろうけど小さい設定ファイル的なものだったらgo-bindataを使用して一緒にビルドしてしまうという手も良さそう。
使っていきたい