go generate を使った assets ディレクトリの静的組み込みバイナリ処理 モノリポで管理しているSPAなクライアント側の成果物 (dist) を gin で静的配布 できないかと思った際に調べた内容。 結局使用していないが、バイナリ化する手順としては使えそうなのでメモしておく。
ディレクトリは以下の様な形
├── Makefile ├── assets -> ../../client/back/dist ├── box │ ├── blob.go │ └── box.go └─generator.go Makefile まずは関係ないけど、Makefileって@書けば && バックスラッシュ連結とかいらんのかという発見 ./... 指定で全てを対象に指定 generate: @go generate ./... @echo "[OK] Files added to embed box!" generator.go 1行目はfmtをかけると自動付与された // +build ignore コメントにより通常ビルド対象から外れる設定 生成される go ファイルは blob.go で box 配下に出力される f, err := os.Create(blobFileName) や、 embedFolder string = "../assets/" から current directory は box 配下の模様 //go:build ignore // +build ignore package main import ( "bytes" "fmt" "go/format" "io/ioutil" "log" "os" "path/filepath" "strings" "text/template" ) const ( blobFileName string = "blob.go" embedFolder string = "../assets/" ) // Define vars for build template var conv = map[string]interface{}{"conv": fmtByteSlice} var tmpl = template.Must(template.New("").Funcs(conv).Parse(`package box // Code generated by go generate; DO NOT EDIT. func init() { {{- range $name, $file := . }} box.Add("{{ $name }}", []byte{ {{ conv $file }} }) {{- end }} }`), ) func fmtByteSlice(s []byte) string { builder := strings.Builder{} for _, v := range s { builder.WriteString(fmt.Sprintf("%d,", int(v))) } return builder.String() } func main() { // Checking directory with files if _, err := os.Stat(embedFolder); os.IsNotExist(err) { log.Fatal("Configs directory does not exists!") } // Create map for filenames configs := make(map[string][]byte) // Walking through embed directory err := filepath.Walk(embedFolder, func(path string, info os.FileInfo, err error) error { relativePath := filepath.ToSlash(strings.TrimPrefix(path, embedFolder)) if info.IsDir() { // Skip directories log.Println(path, "is a directory, skipping...") return nil } else { // If element is a simple file, embed log.Println(path, "is a file, packing in...") b, err := ioutil.ReadFile(path) if err != nil { // If file not reading log.Printf("Error reading %s: %s", path, err) return err } // Add file name to map configs[relativePath] = b } return nil }) if err != nil { log.Fatal("Error walking through embed directory:", err) } // Create blob file f, err := os.Create(blobFileName) if err != nil { log.Fatal("Error creating blob file:", err) } defer f.Close() // Create buffer builder := &bytes.Buffer{} // Execute template if err = tmpl.Execute(builder, configs); err != nil { log.Fatal("Error executing template", err) } // Formatting generated code data, err := format.Source(builder.Bytes()) if err != nil { log.Fatal("Error formatting generated code", err) } // Writing blob file if err = ioutil.WriteFile(blobFileName, data, os.ModePerm); err != nil { log.Fatal("Error writing blob file", err) } } box.go //go:generate go run ../generator.go で go generate 時に実行される処理を記載する プロジェクトルートから ./... で全ファイルが対象として起動されたあと、box.go を見つけて、 先頭に//go:generate... があるからその後ろに定義しているコマンドを実行するような動き //go:generate go run ../generator.go package box type embedBox struct { storage map[string][]byte } // Create new box for embed files func newEmbedBox() *embedBox { return &embedBox{storage: make(map[string][]byte)} } // Add a file to box func (e *embedBox) Add(file string, content []byte) { e.storage[file] = content } // Get file's content func (e *embedBox) Get(file string) []byte { if f, ok := e.storage[file]; ok { return f } return nil } // Embed box expose var box = newEmbedBox() // Add a file content to box func Add(file string, content []byte) { box.Add(file, content) } // Get a file from box func Get(file string) []byte { return box.Get(file) } blob.go go generate 後の成果物 generator.go の template 処理で書いている通り、box.Add でバイナリデータをキー(相対パス)に紐付けるコードが生成されている package box // Code generated by go generate; DO NOT EDIT. func init() { box.Add("js/app.js", []byte{0, 0, ... : blob.goの使用方法 fmt.Println(string(box.Get("js/app.js"))) 参考 The easiest way to embed static files into a binary file in your Golang app (no external dependencies) - DEV Community ...