go:embed 詳解 - 使用編 -
はじめに
Go1.16 にはいつものリリースと同じように興味深い新機能が多数追加されました。その中でも特に注目されている機能として、Go のビルド済みバイナリに読み込み専用の静的ファイルを埋め込む go:embed
ディレクティブがあります。これまで静的ファイルをバイナリに埋め込むアプローチは種々提案されてきましたが[1]、ツールや OS ごとに埋め込み方がバラバラで、それぞれ使い方を覚えたり特定ツールへの依存がどうしても避けられませんでした。今回このディレクティブの導入によって Go 公式として統一される形となります。
このディレクティブ導入が注目される理由として、上記のような歴史的な背景ももちろんありますが、次のような疑問もあると思われます。
- どのような機能があるか?
- どう使うか?(逆にどう使えないか?)
- ファイルはどこに埋め込まれているか?
- ファイルはどのような形式で埋め込まれているか?
- ファイルの埋め込み元から呼び出し元までどのような過程を辿るか?
そこで go:embed
の使い方を説明する 使用編 と、どのような仕組みで実装されているか深ぼる 仕様編 の前編・後編構成に分け、本記事では前編の使い方に的を絞って説明していきます。後編は後日公開するのでお楽しみに!
この記事で使用する Go のバージョンは go1.16 darwin/amd64
です。
階層構造ファイルシステムの統一的インターフェース
go:embed
の説明に入る前に、これまた Go1.16 で導入された io.fs
パッケージについて述べておきます。
階層構造のファイルシステムは、Dennis Ritchie や Ken Thompson がベル研究所で行った UNIX 研究の主だった成果 であり、今日ではオペレーティングシステムはもちろんのこと、Web の URL や ZIP などの書庫ファイル(Vim で一度くらいは開いたことがあるでしょう;))にまで、いたるところで使われています。それにしたがって、Go にも階層構造のファイルシステムに配置されたファイルを読み取る標準パッケージがそれぞれ実装されています。オペレーティングシステムのファイルを操作するのが os
パッケージであり、ZIP は archive/zip
パッケージ、静的ファイルのテンプレートから HTML を動的に生成するのが html/template
パッケージ、URL に対する静的アセットを直接返すのが http
パッケージ(の File/FileSystem
構造体)です。これらは階層構造として同様に扱えるのですが、それぞれ実装が統一されていなかったためなんらかの橋渡しが必要でした。Go が開発されるはるか昔、次世代 UNIX として同じくベル研究所で開発された Plan 9 で階層構造のファイルシステムとして表現されるリソースをプロトコルやアーキテクチャに依存せずに統一的に扱おうとしたにも関わらずです。
そんな中、io/fs
パッケージが流星の如く現れた。Plan 9 の意思を受け継ぎ(?)ようやく Go でもファイルシステムを統一的なインターフェースで扱えるようになりました。
io/fs
の説明や使い方については @spiegel_2007 さんの『次期 Go 言語で導入される(かもしれない) io/fs パッケージについて予習する』 の内容がほぼそのままリリース予定なので詳細はそちらを参照していただくとして、ここから go:embed
の説明に入っていきます。
基本的な使い方
まず初めにコードの実装に入る前に go.mod 中の go のバージョンを 1.16 以上にアップデートします。go 1.15 以下のままだと次のようにコンパイルエラーになってしまいます。ただし go:embed
ディレクティブを使わない場合はこの限りではありません。
go:embed requires go1.16 or later (-lang was set to go1.15; check go.mod)
次にコードを書いていきますが、まずは go:embed
ディレクティブを有効にするために embed
パッケージをインポートします。embed
パッケージを直接利用しない場合はブランクインポートしておきます。
import _ "embed"
次にファイルの埋め込み方について、go:embed
ディレクティブは var
で宣言した初期化されていない変数に対して埋め込むことができます。他のディレクティブと同様に //
と go:embed
の間に半角スペースなどを挟んでしまうと通常のコメントとして扱われてしまうため、注意が必要です。指定できるファイルはカレントディレクトリ配下のファイルで、相対パスで指定します。Windows のような半角バックスラッシュ \
で階層構造を表すプラットフォーム向けにバイナリをビルドする場合でも、*nix 系のように半角スラッシュ /
を使います。
//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
インターフェースを実装している構造体[2]で、単一または複数のファイルやディレクトリを埋め込むことが可能です。複数のファイルやディレクトリを指定するには、ワイルドカード *
を使うか、複数行に分けて指定します。
//go:embed image/* template/*
//go:embed style/*.css
//go:embed html/index.html
var assets embed.FS
最後に go:embed
ディレクティブを埋め込むスコープについてですが、周知の通り変数を宣言できるスコープにはグローバルとローカルの2種類あります。しかし go:embed
で埋め込みが可能なのはそのうちのグローバル変数のみで、ローカル変数に埋め込むことはできません[3]。
// OK
//go:embed global.txt
var global string
func f() {
// NG
//go:embed local.txt
var local string
...
}
go:embed
ディレクティブの簡単な使い方の説明は以上となります。
さてここからは細かい注意点、もっと言うと「意外とこういった使い方をしてもコンパイルエラーにならない」例や逆に「コンパイルエラーにならなそうで実はコンパイルエラーになる」といった例を挙げていきます。
コンパイルエラーにならない例
go:embed
で埋め込める
文字列にキャスト可能な型の変数にも 先ほどは string
と []byte
にしか埋め込めないと述べましたが、それはプロポーザルや go1.16beta1 時点での話。 go1.16rc1 からは文字列にキャスト可能な型、すなわち []uint8
、string, []byte
等を元に定義した型、およびそれらの型エイリアスにも埋め込むことができるようになりました。
//go:embed hoge.txt
var Hoge []uint8
type fuga []byte
//go:embed fuga.txt
var Fuga fuga
type piyo fuga
//go:embed piyo.txt
var Piyo piyo
重複してファイルやディレクトリを参照する
重複して読み込まれるファイルやディレクトリは無視されます。そのため次のように string
型に複数の go:embed
ディレクティブを指定してもコンパイルエラーになりません。
//go:embed hello.txt
//go:embed hello.txt
var hello string
test で埋め込む
文字通り test 内でも埋め込むことができます。
go:embed
ディレクティブがない
embed.FS の前に 単に空のディレクトリとして認識されます。
var uninitialized embed.FS
fmt.Println(uninitialized)
// {<nil>}
f, err := uninitialized.Open(".")
if err != nil {
panic(err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
panic(err)
}
if !fi.IsDir() {
panic("in uninitialized embed.FS, . is not a directory")
}
// You'll get here for sure!
go:embed
ディレクティブとファイルを埋め込む変数の間にスペースがある
(ディレクティブではないですが)cgo ではコメントによる C 言語コードの記述と import "C"
との間にスペースがあると C 言語コードが認識されないのに対し、go:embed
ではスペースがあってもきちんと認識されます。もっとも、スペースが複数行に渡る場合は gofmt
によって1行にフォーマットされるのですが。。
//go:embed hello.txt
var hello.txt
go:embed
ディレクティブとファイルを埋め込む変数の間にコメントがある
上記のスペースの例と同様、コメントがあっても問題はありません。
//go:embed hello.txt
// --- any comments ---
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
ディレクティブでも ..
を使うことが禁止されています。
カレントディレクトリを参照する
意外に思われるかもしれませんが、カレントディレクトリを指す .
も、io/fs
パッケージの階層構造ファイルシステムで禁止されています。したがって embed
パッケージも(以下省略)。これについては禁止にする明示的な理由を示す文献を見つけられませんでした。
イレギュラーなファイル
シンボリックリンクやデバイスファイルなど一部のファイルは go:embed
ディレクティブで埋め込むことができません。具体的には ls -l
でファイル一覧を表示したときに -rw-r--r--
などといった形でファイルモードが表示されると思うのですが、その1文字目が通常ファイル -
でないファイルは埋め込むことができません。他にどのようなファイルが埋め込めないかは ここ に記載されています。
その他
他にも go:embed
ディレクティブで指定できないパターンはいくつかありますが、これを読んでいるあなた自身で見つけるために、楽しみは取っておきましょう;)
おわりに
この記事では go:embed
の使い方や、ファイルパスを指定する際の注意点について述べてきました。次回後編では go:embed
の中身について覗いていきます。
-
io/fs.FS
インターフェースはファイルを開くOpen
関数を備えるだけでファイルシステムとして最小限のことしかできません。embed.FS
構造体はio/fs.FS
インターフェースに加えて、ファイルを読むio/fs.ReadFileFS
と、ディレクトリを読むio/fs.ReadDirFS
も実装しています。 ↩︎ -
プロポーザルや go1.16beta1 まではローカル変数にも埋め込むことができていましたがメモリアロケーションなどの問題でできなくなりました。
https://go-review.googlesource.com/c/go/+/282714/ ↩︎ -
https://golang.org/pkg/io/fs/#ValidPath
https://go.googlesource.com/proposal/+/master/design/draft-embed.md#Dot_dot_module-boundaries_and-file-name-restrictions ↩︎
Discussion