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.gobox 配下に出力される
    • 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.gogo 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