Go でアセットをバイナリに埋め込んでテンプレーティングする

自分のパーソナルプロジェクトで、go の CLI で、テンプレートから、プロジェクトをジェネレートする必要があったので、そこでの学びを整理しておく。Go のプロジェクトで、Yo のようにテンプレートジェネレートをしたい時、go のCLIならきっとシングルバイナリーで決めたいはず。その方法をシェアしたい。

# 戦略

CLI でテンプレートを作成するのは次のようなイメージ。

```
strikes new basic hello-world
```

こうするといくつかのディレクトリとファイルが生成されるというわけ。この時に、テンプレートを作っておいて、hello-world というプロジェクトを作りたい時に、上記のようなコマンドを打つと、basic テンプレートに対して、`hello-world` というパッケージ名を埋めてディレクトリを生成してくれるというイメージ。よくありがちなもの。

実現方法として２つ思いついた。

* テンプレートファイルを作っておいて、それをコードに変換する
* サーバーにテンプレートをおいておいて、展開する

私は開発経験が浅いので素直に師匠に聞くことにした。師匠の答えはこんな感じ。

> go 言語にはなんかコードのジェネレートするようなツールはないんですかねー？

師匠は、サーバー案には触れもしない。間違いなく、コード変換案だ。理由を聞くと

> (師匠) とりあえず、私だと埋め込んじゃいますかねぇ
> (私) 理由は何故ですか？
> (師匠) 簡単だからですかねー
> (私) なるほど w 確かに。正直なところ、埋め込んでヒアドキュメントにしてテンプレートにするのは大した手間じゃないですね。
> (師匠) テンプレート自体はzipなりテキストなりにして別ファイルにしておいて同梱ですかねー ヒアドキュメントはごちゃごちゃしそうなので
> (私) なるほど。Go のいいところは、シングルバイナリになるので、インストールが簡単というところなのですが、その場合は、ビルド時に生成って感じですかね？
> (師匠) C#でいう埋め込まれたリソースみたいなのはない感じですか？

師匠は Go は知らないが、さすがのセンスでアドバイスをくれた。これに該当する物を調べてみた。ところが、本家のライブラリはなく、野良の物しかない様子。せめて野良でも筋のいいのを選びたい。

## アセットを埋め込むライブラリ

* [Choosing A Library to Embed Static Assets in Go](https://tech.townsourced.com/post/embedding-static-files-in-go/)

こちらのブログをじっくり読んでみると次のがおすすめだったので、GitHub の説明とコードを読んで実装してみた。

* [shurcooL/vfsgen](https://github.com/shurcooL/vfsgen)

## テンプレート

テンプレートに関しては標準のがあるので、マニュアルを見ればOK!

* [Package template](https://golang.org/pkg/text/template/)

# 実装

こちらにソースコードをおいておいた。

* [template](https://github.com/TsuyoshiUshio/strikes/tree/master/spikes/template)

## アセットのコードを生成する

まずは、実際のアセットをおいてあるディレクトリを指定して、コードをジェネレートする。デフォルトでは、`main` のパッケージ名が使われて、コマンドを実行したディレクトリに、`go` ファイルがはかれるので、ファイル名や出力先、パッケージも指定したい。`vfsgen.Generate` を使うと、指定したディレクトリを読み込んで、バーチャルファイルシステムを作る go プログラムを生成してくれる。私は `vfsgen.Options` を指定して、ファイル名や、パッケージ名を指定しておいた。こうすると、想定したディレクトリに、go のプログラムを作成してくれる。

```go
	var fs http.FileSystem = http.Dir("./templates/basic")
	options := vfsgen.Options{
		Filename:    "./generated/assets.go",
		PackageName: "assets",
	}
	err := vfsgen.Generate(fs, options)
```

## バーチャルファイルシステムを使う

作成されたファイルは次の感じだ。つまり は Map 仮想ディレクトリを `map[string]interface{}`を作ってくれて実態は、バイトに変換して圧縮して格納してくれているという感じ。

### 生成されたファイル

```go
var assets = func() http.FileSystem {
	fs := vfsgen۰FS{
		"/": &vfsgen۰DirInfo{
			name:    "/",
			modTime: time.Date(2018, 10, 6, 11, 12, 0, 346162909, time.UTC),
		},
		"/circuit": &vfsgen۰DirInfo{
			name:    "circuit",
			modTime: time.Date(2018, 10, 6, 11, 11, 43, 291283315, time.UTC),
		},
		"/circuit/NOTE.txt": &vfsgen۰CompressedFileInfo{
			name:             "NOTE.txt",
			modTime:          time.Date(2018, 10, 6, 11, 11, 43, 288051929, time.UTC),
			uncompressedSize: 237,

			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\xce\xb1\x4e\x03\x31\x10\x04\xd0\xde\x5f\x31\xfc\x80\xaf\xbf\x8e\x86\x50\x22\x1a\x44\x15\x6d\x7c\x73\xc4\x62\xe3\xb5\xbc\x36\x28\x20\xfe\x1d\x1d\x1c\x74\xbb\x23\x8d\xe6\xdd\x53\xd5\xf0\x64\x4d\x17\x54\x49\xaf\xf2\x42\xf8\x48\x89\xee\xeb\x50\xbd\x62\x61\x55\xbb\x72\xb9\x09\xe1\x41\x29\x4e\x34\xae\x6c\xe8\x67\xfe\xdc\x6e\xa3\x25\xfa\x8c\x10\x1e\xf7\x07\x87\x66\xa3\x02\x33\x62\x8c\xe1\xf6\x63\x34\xe2\x6e\x94\xd4\xb3\x15\xdf\xd3\x70\x30\x74\xc3\xb9\xf7\xea\xf3\x34\x7d\xb2\xbc\xe5\x66\xe5\xc2\xd2\x8f\x27\x71\x1e\x8b\x5c\xf8\x25\xb5\x46\xd9\xfa\xef\x3c\x79\xee\xf4\x58\xd8\x27\xa9\x79\xda\xdc\xbf\x6a\x3c\xdb\x40\x92\x02\x27\x37\x15\x44\x15\xeb\xff\xdc\x9f\x3f\x22\x84\xef\x00\x00\x00\xff\xff\xac\xa6\x50\x8b\xed\x00\x00\x00"),
		},
　　　　:
```

### ファイルのオープン

使い方は単純に仮想ファイルを指定して Open すると良い。生成されたファイルに関数ができている。`Open` をすると`File` 構造体が帰ってくるので、普通のファイルのように扱える

```go
	file, err := assets.Open("circuit/manifest.yaml")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	content, _ := ioutil.ReadAll(file)
	p := Package{
		PackageName: "foo",
	}
	err = tmpl.Execute(os.Stdout, p)
         :
```

## ディレクトリの読み込み

上記ので行くと、ファイル名を指定するといけるけど、自分でわざわざディレクトリ情報を記録したくない。生成されたファイルをみるとどう考えても、ディレクトリの一覧は取れそう。こんな感じで可能だった。次のようにすることで、`/circuit` 以下のファイル名が取得できたので、これで、大体必要なことは出来そうだ。

```go
	files, _ := assets.Open("/circuit")
	d, _ := files.Readdir(0)
	for _, fi := range d {
		fmt.Println(fi.Name())
	}
```

# テンプレート

最後のお題は、テンプレート。これらのテンプレートに、パッケージ名を埋め込みたい。これは、先にリンクを貼った公式のライブラリを使えるので楽勝だった。例えば、次のようなテンプレートを用意する。

```yaml
# Strikes package manifest file

name: {{.PackageName}}
description: HelloWorld package for deploying simple C# functions with consumption plan function app.
author: Tsuyoshi Ushio
projectPage: https://github.com/TsuyoshiUshio/hello-world
projectRepo: https://github.com/TsuyoshiUshio/hello-world
```

これに対して、先ほどのライブラリを使いつつ、`{{.PackageName}}` の部分を変数で変換する。まず、`PackageName`を持った`struct` を定義してあげて、template を New したのち、引数としてコンテンツを渡す。ちなみに、`New` の引数は、テンプレート名なので、特になんでも良い。最後に、`Execute` 関数に、 `io.Writer` と、先ほどの struct をインスタンス生成して、値を渡してあげると、テンプレーティング完了。こら簡単だわ。struct を綺麗に作ると、階層を持った設定ファイルみたいなのが作れるだろうな。helm はこれを使ってると思われる。


```go
type Package struct {
	PackageName string
}
   :
	content, _ := ioutil.ReadAll(file)
	p := Package{
		PackageName: "foo",
	}
	tmpl, err := template.New("manifest").Parse(string(content))
	if err != nil {
		panic(err)
	}
        :
        err = tmpl.Execute(os.Stdout, p)
```

## 結果

```yaml
# Strikes package manifest file

name: foo
description: HelloWorld package for deploying simple C# functions with consumption plan function app.
author: Tsuyoshi Ushio
projectPage: https://github.com/TsuyoshiUshio/hello-world
projectRepo: https://github.com/TsuyoshiUshio/hello-world
```

# まとめ

思ったより簡単にテンプレーティングと、アセットの埋め込みができた。野良のライブラリなので若干心配だが、300+スターが付いているし、サポートが無くなっても、テンプレートを作るライブラリはたくさんあるので、ちゃんと綺麗に作っておけば廃れた場合もあまり被害を受けずにすみそうなぐらいシンプルだった。