これは、なにをしたくて書いたもの?
Go 1.16で、embedというパッケージ(機能)が追加されたようなので、こちらを試してみたいなと思いまして。
ビルド時にファイルを埋め込み、アプリケーションの実行時にアクセスできる機能のようです。
embed
Go 1.16のリリースでの、embedに関する情報を並べてみます。
リリースをアナウンスしたブログ。
The new embed package provides access to files embedded at compile time using the new //go:embed directive. Now it is easy to bundle supporting data files into your Go programs, making developing with Go even smoother. You can get started using the embed package documentation. Carl Johnson has also written a nice tutorial, “How to use Go embed”.
Go 1.16 is released - The Go Blog
リリースノートと、embedについて書かれているもの。
Go 1.16 Release Notes - The Go Programming Language
Go 1.16 Release Notes / Embedded Files
Go 1.16 Release Notes / Embedding Files
embedパッケージ。
embed - The Go Programming Language
Goのブログからリンクされていたブログエントリ。
How to Use //go:embed · The Ethically-Trained Programmer
ざっくりした使い方は、embedパッケージのOverviewおよびDirectivesを見るとよいと思います。
embed - The Go Programming Language
使い方は、こんな感じみたいです。
- embedパッケージを
import
する - 埋め込むファイルやディレクトリを、
//go:embed [パス]
というコメント(ディレクティブ)でvar
に対して指定する - ファイルやディレクトリを埋め込む
var
で使える型- 単一のファイル
string
byte
のスライス
- ファイル(複数可)またはディレクトリ
embed.FS
- 単一のファイル
string
やbyte
のスライスに埋め込むする場合は、embedパッケージは直接使わないのでimport
は以下のようにすれば
OKみたいです。
※import
自体は必要です
import _ "embed"
あとは、実際に試してみましょうか。
環境
今回の環境は、こちらです。
$ go version go version go1.16.2 linux/amd64
確認用のプロジェクトを作成。
$ go mod init embed-example go: creating new go.mod: module embed-example
go.mod
module embed-example go 1.16
試してみる
では、embedパッケージの機能を試してみましょう。
最初は、string
とbyte
のスライスに対して、ファイルの中身を埋め込んでみます。
hello.txt
Hello Go!!
こんなプログラムを作成。
main.go
package main import ( _ "embed" "fmt" ) var ( //go:embed hello.txt message string //go:embed hello.txt messageBinary []byte ) func main() { fmt.Printf("embedded as string: %s\n", message) fmt.Printf("embedded as binary: %#v\n", messageBinary) }
こんなディレクティブ付きのvar
で、ファイルの中身を埋め込めます。
var ( //go:embed hello.txt message string //go:embed hello.txt messageBinary []byte )
こういうのを、ディレクティブって言うんですね。
Command compile / Compiler Directives
この時、embed
パッケージをimport
しておくのがポイントです。embed
パッケージ自体は使わないので、_
としています。
import ( _ "embed" "fmt" )
実行結果。
$ go run main.go embedded as string: Hello Go!! embedded as binary: []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x47, 0x6f, 0x21, 0x21, 0xa}
ファイルの中身がvar
に入っていますね。
次は、embed.FS
を使ってみましょう。今のmain
関数は1度リネームして、
func __main() {
ファイル名もリネーム。
$ mv main.go __main.go
ディレクトリを作成しましょう。
$ mkdir -p resources/sub
この中に、ファイルを作っていきます。
$ echo message1 > resources/message1.txt $ echo message2 > resources/message2.txt $ echo greeting > resources/sub/greeting.txt
全部で、ファイルはこれだけです。
$ find hello.txt resources -type f
hello.txt
resources/message2.txt
resources/sub/greeting.txt
resources/message1.txt
では、新しくプログラムを作成。
main.go
package main import ( "embed" "fmt" "log" ) var ( //go:embed hello.txt file embed.FS //go:embed resources/message1.txt resources/message2.txt messages embed.FS //go:embed resources/*.txt messagesUsingWildcard embed.FS //go:embed resources resourcesDir embed.FS ) func main() { fmt.Println("========== file ==========") contents, err := file.ReadFile("hello.txt") if err != nil { log.Fatal(err) } fmt.Printf("file contents: %s\n", string(contents)) fmt.Println("========== messages ==========") entries, err := messages.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { c, err := messages.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) } fmt.Println("========== messagesUsingWildcard ==========") entries, err = messagesUsingWildcard.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { c, err := messagesUsingWildcard.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) } fmt.Println("========== resourcesDir ==========") entries, err = resourcesDir.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { if !entry.IsDir() { c, err := messages.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) } } entries, err = resourcesDir.ReadDir("resources/sub") if err != nil { log.Fatal(err) } for _, entry := range entries { if !entry.IsDir() { c, err := resourcesDir.ReadFile("resources/sub/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/sub/%s, contents: %s\n", entry.Name(), string(c)) } } }
今後は、embed
パッケージを使います。
import ( "embed" "fmt" "log" )
embed.FS
型の変数に対して、ファイルやディレクトリを指定します。
var ( //go:embed hello.txt file embed.FS //go:embed resources/message1.txt resources/message2.txt messages embed.FS //go:embed resources/*.txt messagesUsingWildcard embed.FS //go:embed resources resourcesDir embed.FS )
embed.FS
は、読み取り専用のファイルのコレクションです。
main.go
package main import ( _ "embed" "fmt" ) var ( //go:embed hello.txt message string //go:embed hello.txt messageBinary []byte ) func main() { fmt.Printf("embedded as string: %s\n", message) fmt.Printf("embedded as binary: %#v\n", messageBinary) }
embed.FS
からは、ファイルやディレクトリを開いたりできます。
単一のファイルを埋め込み、
//go:embed hello.txt
file embed.FS
開いた場合。
fmt.Println("========== file ==========") contents, err := file.ReadFile("hello.txt") if err != nil { log.Fatal(err) } fmt.Printf("file contents: %s\n", string(contents))
結果。
========== file ========== file contents: Hello Go!!
複数のファイルを埋め込んだ場合。
//go:embed resources/message1.txt resources/message2.txt messages embed.FS //go:embed resources/*.txt messagesUsingWildcard embed.FS
スペース区切りで複数指定したり、path.Match
で使えるパターンで指定できたりします。
今回はやっていませんが、複数回の指定も可能みたいです。
// content holds our static web server content. //go:embed image/* template/* //go:embed html/index.html var content embed.FS
パスの区切り文字は/
(Windowsでも)で、.
や..
を含めることはできません。/
で開始したり、/
で終了することも
許可されません。ディレクトリ内のファイルをすべて含めるには、*
を使います。
The patterns are interpreted relative to the package directory containing the source file. The path separator is a forward slash, even on Windows systems. Patterns may not contain ‘.’ or ‘..’ or empty path elements, nor may they begin or end with a slash. To match everything in the current directory, use ‘*’ instead of ‘.’.
今回は階層を含むので、ディレクトリも認識してくれます。
fmt.Println("========== messages ==========") entries, err := messages.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { c, err := messages.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) } fmt.Println("========== messagesUsingWildcard ==========") entries, err = messagesUsingWildcard.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { c, err := messagesUsingWildcard.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) }
結果。
========== messages ========== name: resources/message1.txt, contents: message1 name: resources/message2.txt, contents: message2 ========== messagesUsingWildcard ========== name: resources/message1.txt, contents: message1 name: resources/message2.txt, contents: message2
ディレクトリそのものを埋め込む場合。
//go:embed resources
resourcesDir embed.FS
この場合、対象のディレクトリおよびサブディレクトリが再帰的に埋め込まれます。特定ディレクトリ配下のファイルを公開する
Webサーバーのような用途に合いそうですね。
If a pattern names a directory, all files in the subtree rooted at that directory are embedded (recursively), except that files with names beginning with ‘.’ or ‘_’ are excluded.
ディレクトリエントリを再帰的に埋め込むものの、.
または_
で始まるファイルエントリは含まれません。
ワイルドカード(*
)や直接ファイルを指定した場合との差は、.
などで始まるファイルに現れることになります。
埋め込んだファイルやディレクトリにアクセスするコード。
fmt.Println("========== resourcesDir ==========") entries, err = resourcesDir.ReadDir("resources") if err != nil { log.Fatal(err) } for _, entry := range entries { if !entry.IsDir() { c, err := messages.ReadFile("resources/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/%s, contents: %s\n", entry.Name(), string(c)) } } entries, err = resourcesDir.ReadDir("resources/sub") if err != nil { log.Fatal(err) } for _, entry := range entries { if !entry.IsDir() { c, err := resourcesDir.ReadFile("resources/sub/" + entry.Name()) if err != nil { log.Fatal(err) } fmt.Printf("name: resources/sub/%s, contents: %s\n", entry.Name(), string(c)) } }
実行結果。
========== resourcesDir ========== name: resources/message1.txt, contents: message1 name: resources/message2.txt, contents: message2 name: resources/sub/greeting.txt, contents: greeting
go:embed
で直接指定していないサブディレクトリも含めて、アクセスできていることが確認できます。
ざっと、使い方はこんな感じでしょうか。
もう少し挙動や中身を見てみる
基本的なことはわかった気がするものの、もうちょっと気になるところを追ってみましょう。
確認するファイルを、こちらに戻します。
main.go
package main import ( _ "embed" "fmt" ) var ( //go:embed hello.txt message string //go:embed hello.txt messageBinary []byte ) func main() { fmt.Printf("embedded as string: %s\n", message) fmt.Printf("embedded as binary: %#v\n", messageBinary) }
たとえば、//
とgo:embed
の間にスペースを入れてみます。
var ( // go:embed hello.txt message string //go:embed hello.txt messageBinary []byte )
すると、ディレクティブとして認識しなくなり、値が入らなくなります。
$ go run main.go embedded as string: embedded as binary: []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x47, 0x6f, 0x21, 0x21, 0xa}
ディレクティブは、そういうものみたいですね。
Command compile / Compiler Directives
次。関数内のvar
にディレクティブを指定してみます。
func main() { fmt.Printf("embedded as string: %s\n", message) fmt.Printf("embedded as binary: %#v\n", messageBinary) //go:embed hello.txt var localMessage string }
これは、エラーになります。
$ go run main.go # command-line-arguments ./main.go:21:4: go:embed cannot apply to var inside func
ディレクティブに指定したファイルが存在しない場合。
var ( //go:embed hello.tx message string //go:embed hello.txt messageBinary []byte )
これもエラーになります。
$ go run main.go main.go:9:13: pattern hello.tx: no matching files found
go:embed
ディレクティブを使っているのに、embed
パッケージのインポートを忘れた場合。
import ( // _ "embed" "fmt" )
これも、エラーになります。
$ go run main.go # command-line-arguments ./main.go:9:4: //go:embed only allowed in Go files that import "embed" ./main.go:12:4: //go:embed only allowed in Go files that import "embed"
こういったチェックは、このあたりのファイルで行われています。
https://github.com/golang/go/blob/go1.16.2/src/cmd/compile/internal/gc/embed.go
pos := embeds[0].Pos if !haveEmbed { p.yyerrorpos(pos, "invalid go:embed: missing import \"embed\"") return } if len(names) > 1 { p.yyerrorpos(pos, "go:embed cannot apply to multiple vars") return } if len(exprs) > 0 { p.yyerrorpos(pos, "go:embed cannot apply to var with initializer") return } if typ == nil { // Should not happen, since len(exprs) == 0 now. p.yyerrorpos(pos, "go:embed cannot apply to var without type") return } if dclcontext != PEXTERN { p.yyerrorpos(pos, "go:embed cannot apply to var inside func") return }
このあたりを見ると、書けない場所はわかりそうですね。
https://github.com/golang/go/blob/go1.16.2/src/cmd/compile/internal/gc/noder.go
ドキュメントにもありましたが、相対パス指定で上位ディレクトリを参照することはできません。
var ( //go:embed ../hello.txt message string //go:embed hello.txt messageBinary []byte )
エラーになります。
$ go run main.go main.go:9:13: pattern ../hello.txt: invalid pattern syntax
このチェックをしているのは、こちらです。
https://github.com/golang/go/blob/go1.16.2/src/cmd/go/internal/load/pkg.go#L2087-L2089
.
以外については、fs.ValidPath
を使って確認しています。
該当のパッケージで使われているgo:embed
なファイルやディレクトリは、go list
で確認することができます。
以下のフィールドが該当します。
// Embedded files EmbedPatterns []string // //go:embed patterns EmbedFiles []string // files matched by EmbedPatterns TestEmbedPatterns []string // //go:embed patterns in TestGoFiles TestEmbedFiles []string // files matched by TestEmbedPatterns XTestEmbedPatterns []string // //go:embed patterns in XTestGoFiles XTestEmbedFiles []string // files matched by XTestEmbedPatterns
現在のパッケージに含まれるEmbedPatterns
やEmbedFiles
は、以下で確認できます。
$ go list -f '{{.EmbedPatterns}}' $ go list -f '{{.EmbedFiles}}'
実行例。
$ go list -f '{{.EmbedPatterns}}' [hello.txt resources resources/*.txt resources/message1.txt resources/message2.txt] $ go list -f '{{.EmbedFiles}}' [hello.txt resources/message1.txt resources/message2.txt resources/sub/greeting.txt]
こちらのコマンドでも見れます。
$ go list -json
EmbedPatterns
とEmbedFiles
の結果の例だけ載せておきます。
"EmbedPatterns": [ "hello.txt", "resources", "resources/*.txt", "resources/message1.txt", "resources/message2.txt" ], "EmbedFiles": [ "hello.txt", "resources/message1.txt", "resources/message2.txt", "resources/sub/greeting.txt" ],
ただ、プロジェクト全体で埋め込んでいるものがわかるかというとそうでもなく、あくまでパッケージ単位です。
各パッケージごとに確認する必要があります。
まとめ
Go 1.16で追加されたembedパッケージを使って、Goプログラムにファイルやディレクトリを埋め込むことができることを
確認してみました。
いいですね、この機能。
使いそうなシーンも多そうなので、覚えておきましょう。