go:embed 詳解 - 使用編 -

:warning::warning:  **go1.16rc1 以降は 👉[コチラ](https://zenn.dev/koya_iwamura/articles/53a4469271022e) で更新していきます。** :warning::warning:

この記事は [Go Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go) 23日目 と [CyberAgent Developers Advent Calendar 2020](https://adventar.org/calendars/5711) 24日目 の記事です。

## はじめに

[来年の2月にリリース予定の Go1.16](https://tip.golang.org/doc/go1.16) にはいつものリリースと同じように興味深い新機能が多数追加される。その中でも特に注目されている機能として、Go のビルド済みバイナリに読み込み専用の静的ファイルを埋め込む `go:embed` ディレクティブがある。これまで静的ファイルをバイナリに埋め込むアプローチは種々提案されてきたが[^1]、ツールやOSごとに埋め込み方がバラバラで、それぞれ使い方を覚えたり、特定ツールへの依存がどうしても避けられなかった。今回このディレクティブの導入により Go 公式として統一される形となる。

このディレクティブ導入が注目される理由として、上記のような歴史的な背景ももちろんあるが、次のような疑問もあると思う。

- どのような機能があるか？
- どう使うか？（逆にどう使えないか？）
- ファイルはどこに埋め込まれているか？
- ファイルはどのような形式で埋め込まれているか？
- ファイルの埋め込み元から呼び出し元までどのような過程を辿るか？

そこで `go:embed` の使い方を説明する **使用編** と、どのような仕組みで実装されているか深ぼる **仕様編** の前編・後編構成に分け、本記事では前編の使い方に的を絞って説明していく。後編はお楽しみにということで後日公開することにする。

この記事で使用する Go のバージョンは `go1.16beta1 darwin/amd64` で、執筆時点ではまだ Go1.16 が正式リリースされていないため、この記事の内容から更新される場合があることに注意されたい。

[^1]: [ドラフト](https://go.googlesource.com/proposal/+/master/design/draft-embed.md#background)に書かれているだけでもたくさんある。

## 階層構造ファイルシステムの統一的インターフェース
`go:embed` の説明に入る前に、これまた Go1.16 で導入予定の `io.fs` パッケージについて述べておく。

階層構造のファイルシステムは、Dennis Ritchie や Ken Thompson がベル研究所で行った [UNIX 研究の主だった成果](https://people.eecs.berkeley.edu/~brewer/cs262/unix.pdf) であり、今日ではオペレーティングシステムはもちろんのこと、Web の URL や ZIP などの書庫ファイル（Vim で一度くらいは開いたことがあるでしょう;)）にまで、いたるところで使われている。それにしたがって、Go にも階層構造のファイルシステムに配置されたファイルを読み取る標準パッケージがそれぞれ実装されている。オペレーティングシステムのファイルを操作するのが `os` パッケージであり、ZIP は `archive/zip` パッケージ、静的ファイルのテンプレートから HTML を動的に生成するのが `html/template` パッケージであり、URL に対する静的アセットを直接返すのが `http` パッケージ（の `File/FileSystem` 構造体）だ。これらは階層構造として同様に扱えるにも関わらず、それぞれ実装が統一されていなかったため、なんらかの橋渡しが必要であった。次世代 UNIX として同じくベル研究所で開発された [Plan 9](https://xtech.nikkei.com/it/free/NSW/NEWS/20001206/1/) で階層構造のファイルシステムとして表現されるリソースをプロトコルやアーキテクチャに依存せずに統一的に扱おうとしたにも関わらずである。

そんな中、`io/fs` パッケージが流星の如く現れた。Plan 9 の意思を受け継ぎ（[？](https://www.atmarkit.co.jp/news/analysis/200902/09/future.html)）ようやく Go でもファイルシステムを統一的なインターフェースで扱えるようになったのだ。

`io/fs` の説明や使い方については [@spiegel_2007](https://twitter.com/spiegel_2007) さんの『[次期 Go 言語で導入される（かもしれない） io/fs パッケージについて予習する](https://text.baldanders.info/golang/file-system-interface-in-go-2/)』 の内容がほぼそのままリリース予定なので、詳細はそちらを参照していただこう。

## 使い方
### 基本的な使い方

さて、ここから本題に入る。

まずはじめに、`go:embed` ディレクティブを有効にするために `embed` パッケージをインポートする。`embed` パッケージを直接利用しない場合はブランクインポートしておく。

```go
import _ "embed"
```

次にファイルの埋め込み方について、`go:embed` ディレクティブは `var` で宣言した初期化されていない変数に対して埋め込むことができる。他のディレクティブと同様に `//` と `go:embed` の間に半角スペースなどを挟んでしまうと通常のコメントとして扱われてしまうため、注意が必要だ。指定できるファイルはカレントディレクトリ配下のファイルで、相対パスで指定する。Windows のような半角円記号 `¥` で階層構造を表すプラットフォーム向けにバイナリをビルドする場合でも、*nix 系のように半角スラッシュ `/` で表す。

```go
//go:embed hello.txt
var hello string

//go:embed hello/world.txt
var world []byte
```

`go:embed` ディレクティブで埋め込める変数の型は、`string`、`[]byte`、`embed.FS` の3種類であるが、前者2つと最後の1つでは埋め込み方に違いがある。

`string` と `[]byte` は単一の `go:embed` ディレクティブによってファイルを読み込み、通常通り初期化を行った変数として扱うことが可能となる。直感に即す通り、複数の `go:embed` を定義するとコンパイルエラーになる。

一方で `embed.FS` は `go:embed` で埋め込むファイルを階層型ファイルシステムとして埋め込むことができる、すなわち `io/fs.FS` インターフェースを実装している構造体[^3]で、単一または複数のファイルやディレクトリを埋め込むことが可能である。複数のファイルやディレクトリを指定するには、ワイルドカード `*` を使うか、複数行に分けて指定する。

[^3]: `io/fs.FS` インターフェースはファイルを開く `Open` 関数を備えるだけでファイルシステムとして最小限のことしかできない。`embed.FS` 構造体は `io/fs.FS` インターフェースに加えて、ファイルを読む `io/fs.ReadFileFS` と、ディレクトリを読む `io/fs.ReadDirFS` も実装している。

```go
//go:embed image/* template/*
//go:embed style/*.css
//go:embed html/index.html
var assets embed.FS
```

最後に `go:embed` ディレクティブを埋め込むスコープについてだが、グローバルで埋め込むことができるのはもちろん、ローカルすなわち関数内でも埋め込むことは可能である。ローカルで埋め込むのは若干違和感を覚えるものの、通常の変数の初期化と同じように考えるとごく自然である[^local]。

[^local]: ローカル変数へのディレクティブによる埋め込みは、関数間で埋め込んだファイルを共有するか、関数ごとに埋め込んだファイルを分けるか、など問題が出てきたため Go1.16 では実装から削除される方向で進んでいる。 https://github.com/golang/go/issues/43216

```go
//go:embed global.txt
var global string

func f() {
	//go:embed local.txt
	var local string
	...
}
```

`go:embed` ディレクティブの簡単な使い方の説明は以上となる。

さてここからは細かい注意点、もっと言うと「意外とこういった使い方をしてもコンパイルエラーにならない」例や逆に「コンパイルエラーにならなそうで実はコンパイルエラーになる」といった例を挙げていく。

### コンパイルエラーにならない例
#### 重複してファイルやディレクトリを参照する

重複して読み込まれるファイルやディレクトリは無視される。それ故、次のように `string` 型に複数の `go:embed` ディレクティブを指定してもコンパイルエラーにならない。

```go
//go:embed hello.txt
//go:embed hello.txt
var hello string
```

#### test で埋め込む

文字通り test 内でも埋め込むことは可能である。

#### embed.FS の前に `go:embed` ディレクティブがない

単に空のディレクトリとして認識されるだけである。

```go
var fs embed.FS

fmt.Println(fs)
// {<nil>}
```

#### `go:embed` ディレクティブとファイルを埋め込む変数の間にスペースがある

（ディレクティブではないが）cgo では、コメントによる C 言語コードの記述と `import "C"` との間にスペースがあると C 言語コードが認識されないのに対し、`go:embed` ではスペースがあってもきちんと認識される。もっとも、スペースが複数行に渡る場合は `gofmt` によって1行にフォーマットされるのだが。

```go
//go:embed hello.txt

var hello.txt
```

#### `go:embed` ディレクティブとファイルを埋め込む変数の間にコメントがある

上記のスペースの例と同様、コメントがあっても問題はない。

```go
//go:embed hello.txt
// --- comment ---
var hello.txt
```

### コンパイルエラーになる例

#### 空のディレクトリを参照する

`go:embed` ディレクティブを指定せずに `embed.FS` の変数を宣言した時のように、空のディレクトリも埋め込むことが可能であると思いきやコンパイルエラーになる。上記で散々 **ディレクトリを埋め込む** と述べて来たが、実は `go:embed` ディレクティブで埋め込む単位は **ファイル** であり、ディレクトリ自体ではない。`go:embed` ディレクティブを指定せずに宣言した `embed.FS` の変数は、元の構造体を見ていただければ分かるが、`file` 型のスライスをメンバに持ち、単にこのスライスに対して明示的に初期化しないが故に0個のファイルを持つ状態になるのに対し、空のディレクトリを埋め込む場合はディレクトリの先に1個以上ファイルがあるのを期待するためコンパイルエラーとなる。Git で空のディレクトリをコミットできないことを想像していただければ分かりやすいであろう。後編で詳しく触れるが、埋め込むファイルは package と同じような扱いであり、空のディレクトリである package をインポートしたときにコンパイルエラーになる（この場合もはや package と呼べるか怪しいが）のと同様に、空ディレクトリを埋め込んだときもコンパイルエラーになる。

#### ディレクトリのパスを `/` で終わる

これも埋め込むのはファイルであってディレクトリではないため、コンパイルエラーになる。ディレクトリ内のファイルを全て埋め込みたいのであれば、ワイルドカードで指定する。

#### `embed` をインポートせずに `go:embed` ディレクティブを使う

`github.com/go-sql-driver/mysql` や `github.com/mattn/go-sqlite3` のような SQL Driver を使う際にしばしばブランクインポートをするのとは別の理由で、コンパイル時にそう定めているためである。

#### 存在しないファイルを参照しようとする

これは特段説明はいらないだろう。

#### 親ディレクトリを参照する
`io/fs` パッケージでは、依存するファイルが完全にそれ配下の階層構造ファイルシステムにあることを期待するため、親ディレクトリを参照可能にする `..` をパスに含めることは禁止されている[^4]。`embed` パッケージも `io/fs` パッケージが提供するインターフェースに則った階層構造ファイルシステムの構造体を実装しているため、`go:embed` ディレクティブでも `..` を使うことが禁止される。

[^4]: https://tip.golang.org/pkg/io/fs/#ValidPath <br> https://go.googlesource.com/proposal/+/master/design/draft-embed.md#Dot_dot_module-boundaries_and-file-name-restrictions

#### カレントディレクトリを参照する

意外に思われるかもしれないが、カレントディレクトリを指す `.` も、`io/fs` パッケージの階層構造ファイルシステムで禁止されている。したがって `embed` パッケージも（以下省略）。これについては、禁止にする明示的な理由を示す文献を見つけられなかった。

#### イレギュラーなファイル

シンボリックリンクやデバイスファイルなど、一部のファイルは `go:embed` ディレクティブで埋め込むことができない。具体的には `ls -l` でファイル一覧を表示したときに `-rw-r--r--` などといった形でファイルモードが表示されると思うが、その1文字目が通常ファイル `-` でないファイルは埋め込むことができない。他にどのようなファイルが埋め込めないかは [ここ](https://github.com/golang/go/blob/bc7e4d9257693413d57ad467814ab71f1585a155/src/io/fs/fs.go#L183) に記載されている。

### その他

他にも `go:embed` ディレクティブで指定できないパターンはいくつかあるが、これを読んでいるあなた自身が見つけるために、楽しみは取っておこう。

## おわりに

この記事では `go:embed` の使い方や、ファイルパスを指定する際の注意点について述べてきた。次回後編では `go:embed` の中身について覗いていく。