Goのembedを使う

はじめに

Goの1.16からembed packageがcoreライブラリに追加されています。結構面白い感じの機能だったのでちょっと試してみようかと思います。

go enbedとは

embedを利用すると静的ファイルをGoのプログラムに埋め込み、そこに対するアクセスを提供してくれます。embedでは以下の3つの形式でファイルを読み込むことができます

  • string
  • byte[]
  • FS

ファイルの読み込みはパッケージのディレクトリかもしくはそのサブディレクトリから読み込むことができます。

使ってみる。

環境

動作環境は以下です。

$ go version
go version go1.16 linux/amd64

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

単体のテキストファイルをstringやbyte[]として読み込む

まずは基本的な使い方として単体のテキストファイルの読み込みをしてみます。
//go:embedディレクティブをコメントとして記述することで利用することができます。
まずは以下のようなテキストファイルをembedディレクティブを記述するプログラムと同じファイル階層に置いておきます。

hello.txt

hello, world.
this is embedded.

このテキストファイルをembedでstringとして、読み込むためには以下のようにします。

package main

import (
    _ "embed"
    "fmt"
)

//go:embed hello.txt
var hello string

func main() {
    fmt.Println(hello)
}

実行してみます。

$ go build -o hello_embed
$ ./hello_embed 
hello, world.
this is embedded.

注目ポイントはふたつで、まずはembedを利用するためにパッケージのブランクインポートが必要です。
もう一つのポイントはembedディレクティブは関数の外側でパッケージグローバルに定義しておく必要があるということで例えばプログラムを以下のように変更するとエラーで落ちます。

func main() {
    //go:embed hello.txt
    var hello string
    fmt.Println(hello)
}
$ go build -o hello_embed
# github.com/samuraiball/go-sandbox
./main.go:10:4: go:embed cannot apply to var inside func

読み込んだファイルを[]byteで受け取りたい場合は単に以下のように書き換えるだけで大丈夫です。

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

func main() {
    fmt.Println(hello)
    fmt.Println(string(hello))
}

実行結果

[104 101 108 108 111 44 32 119 111 114 108 100 46 10 116 104 105 115 32 105 115 32 101 109 98 101 100 100 101 100 46]
hello, world.
this is embedded.

複数ファイルの読み込み

1つ以上の複数ファイルを読み込みたい場合はembed.FSを受け取りの型として利用することができます。
例えばhtmlディレクトリをembedを行なうプログラムファイルと同じ階層に作り以下の2つを用意します。

index1.html

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>

index2.thml

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

まずindex1.htmlと、index2.htmlを読み込むためには以下のようにします。

//go:embed html/*
var html embed.FS

func main() {

    htmlBytes, err := html.ReadFile("html/index1.html")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf(string(htmlBytes))

}

前述している通り、embed.FSで受け取ります。そして、個別のファイルを受け取る場合は[func (f FS) ReadFile(name string) ([]byte, error)](https://golang.org/pkg/embed/#FS.ReadFile)を利用し、読み込みたいディレクトリ/ファイル名を文字列で渡すと利用することができます。この戻り値は []byteになります。

実行結果は以下の通りになります。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

ここで、ファイルの読み込みはpath.Matchパターンでファイルを読み込むことができます例えば//go:embed html/*1.htmlに書き換えて先程のコードを実行すると、index1.htmlが読み込まれるようになるため、index2, err := html.ReadFile("html/index2.html")のところで落ちるようになります。

2021/03/05 19:12:12 open html/index2.html: file does not exist

また、embed.FSには他にも、func (f FS) Open(name string) (fs.File, error)func (f FS) ReadDir(name string) ([]fs.DirEntry, error)も定義されており、それぞれfs.Fileでファイルを読み込んだり、[]fs.DirEntryを読み込んだりすることができます。

複数ディレクトリからまとめてファイルを読み込む

embedは複数のディレクトリからファイルを読み込むことも可能です。
先程のindex1.htmlindex2.htmlをそれぞれhtml1html2ディレクトリを作成して格納し、それらを読み込みたい場合は以下のようにします。

//go:embed html1/* html2/*
var html embed.FS

func main() {

    index1, err := html.ReadFile("html1/index1.html")
    // エラーハンドリング省略

    index2, err := html.ReadFile("html2/index2.html")
    // エラーハンドリング省略

    fmt.Print(string(index1))

    fmt.Println()
    fmt.Printf("----------------------------------------------------")
    fmt.Println()

    fmt.Print(string(index2))
}

実行結果は先ほどと同じです。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>