CLOVER🍀

That was when it all began.

Go 1.16で追加された、embedパッケージ(Embedded Files)を試してみる

これは、なにをしたくて書いたもの?

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

embed · pkg.go.dev

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は、読み取り専用のファイルのコレクションです。

Package embed / type 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で使えるパターンで指定できたりします。

Package embed / Directives

Package path / func 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 ‘.’.

Package embed / Directives

今回は階層を含むので、ディレクトリも認識してくれます。

   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.

Package embed / Directives

ディレクトリエントリを再帰的に埋め込むものの、.または_で始まるファイルエントリは含まれません。
ワイルドカード(*)や直接ファイルを指定した場合との差は、.などで始まるファイルに現れることになります。

埋め込んだファイルやディレクトリにアクセスするコード。

   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を使って確認しています。

Package fs / func 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プログラムにファイルやディレクトリを埋め込むことができることを
確認してみました。

いいですね、この機能。

使いそうなシーンも多そうなので、覚えておきましょう。