見出し画像

Go の embed パッケージが便利だったので紹介します

こんにちは、 Zaim でサーバーサイドエンジニアをしている @hira です。
Go 1.16 で追加された embed パッケージ についてはご存知でしょうか?
私は、このパッケージを使って簡単な Web サイトを作成しました。
サーバーから配信したいアセットファイルなどを簡単に Go のバイナリに埋め込むことができて便利だったので、今回は embed パッケージについて紹介します。

ファイルをバイナリに埋め込める embed パッケージ

embed パッケージは設定ファイルやアセットファイルを Go の実行バイナリに埋め込めるパッケージです。
ファイルの読み込みだけであれば os パッケージの ReadFile 関数を使っても実現できますが、 embed パッケージを使うことでファイルの内容を直接コードで扱うことができるようになります。
ファイルの内容は実行バイナリに埋め込まれるため、ファイル読み取り時に発生するコストも削減できます。
また、Go のコードとファイルを分離せずに済むので、ビルド後のファイルの依存関係を意識する必要がないといったメリットもあります。

embed パッケージの使い方

ここからは embed パッケージの使い方について説明します。
embed パッケージを使ったファイルの埋め込みには「単一ファイルの内容をバイト配列として埋め込む方法」と「複数のファイルを Go のファイルシステム」として埋め込む方法の 2 つがあります。
それぞれの方法について紹介したいと思います。

1. 単一ファイルをバイト配列として埋め込む方法

単一ファイルは次の手順でバイト配列に埋め込めます。

  1. _ をつけて embed パッケージをインポートする

  2.  go:embed ディレクティブで埋め込むファイルを指定する

  3.  ファイルの内容を埋め込むバイト配列を定義する

以下は config.json から読み込んだ内容をバイト配列に埋め込み、構造体にデコードするコードの例です。

config.json は以下の通りです。

{
  "setting1": "setting1",
  "setting2": "setting2",
  "setting3": "setting3"
}
package main

import (
	_ "embed" // ① _ をつけて embed パッケージをインポート
	"fmt"
)

//go:embed config.json ② go:embed ディレクティブで埋め込むファイルを指定
var configByte []byte ③ バイト配列を定義

// configByte をデコードするための構造体
type Config struct {
	Field1 string `json:"setting1"`
	Field2 string `json:"setting2"`
	Field3 string `json:"setting3"`
}

func main() {
	fmt.Println(string(configByte))

	cfg := &Config{}
	err := json.Unmarshal(configByte, cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Field1: %s\n", cfg.Field1)
	fmt.Printf("Field2: %s\n", cfg.Field2)
	fmt.Printf("Field3: %s\n", cfg.Field3)
}

実行結果は次の通りです。

{
  "setting1": "setting1",
  "setting2": "setting2",
  "setting3": "setting3"
}

Field1: setting1
Field2: setting2
Field3: setting3

ファイルの内容を埋め込むためのバイト配列を定義して go:embed ディレクティブでファイルを指定するだけなので、Go で単一の設定ファイルを読み込みたいケースに便利ですね!
実際にファイルの内容がバイト配列に埋め込まれることを確認したい場合は、上記のコードをビルド後、go:embed ディレクティブで指定しているファイルを削除してみましょう。
ファイル削除後にバイナリを実行しても正常に動作することを確認できるはずです。

2. 複数のファイルをファイルシステムとして埋め込む方法

複数のファイルを埋め込む場合は embed.FS 型として定義した変数に対して埋め込みます。

手順としては以下の通りです。

  1.  embed パッケージをインポートする(_ なし)

  2. go:embed ディレクティブを使ってファイルを指定する(ワイルドカード可)

  3. embed.FS 型の変数を定義する

以下のディレクトリ構成のプロジェクトで public ディレクトリ以下のファイルを Go のファイルシステムとして埋め込む方法を例に説明します。

project/
├ public/
          ├ index.html
          └ sample.png
└ main.go

index.html の中身は以下の通りで、同じディレクトリにあった sample.png を表示します。

<html>
    <body>
        <h1>index.html</h1>
        <img src="sample.png" />
    </body>
</html>

index.html を配信する Go のコードは以下の通りになります。

package main

import (
	"embed" // ① embed パッケージをインポート
	"io/fs"
	"log"
	"net/http"
)

//go:embed public/* // ② go:embed ディレクティブで埋め込むファイルを指定
var publicFS embed.FS // ③ embed.FS 型の変数を定義

func main() {
	// fs パッケージの FS として使うことができる
	public, err := fs.Sub(publicFS, "public")
	if err != nil {
		panic(err)
	}
	http.Handle("/", http.FileServer((http.FS(public))))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

単一ファイルの内容を埋め込むケースとほとんど同じですが、複数のファイルを埋め込むための変数の型が embed.FS 型になります。
embed.FS は fs パッケージの FS インターフェースを実装するので、 Go のファイルシステムが使えるところであれば、どこでも使えます。
最近はフロントエンドとバックエンドでサーバーを分けるケースが多いですが、 embed パッケージを使って Go だけで Web ページを配信するサーバーを作ってみるのも良いかもしれません。
注意点として、大量のアセットファイルを embed パッケージで埋め込むとアセットファイル分だけ実行バイナリが大きくなりますので、大規模な Web サイトなどには向かないと思います。

パフォーマンス

embed パッケージを使った場合はファイルの内容がメモリに展開されるため os パッケージを使ってファイルを読み込むよりもパフォーマンスが良くなります。
実際に Go の testing パッケージを使ってベンチマークを計測してみました。
計測に使用するコードは設定ファイルの内容を構造体にデコードするという処理です。

//go:embed config.json
var configByte []byte

type Config struct {
	Field1 string `json:"setting1"`
	Field2 string `json:"setting2"`
	Field3 string `json:"setting3"`
}

// embed パッケージを使ってバイト配列に埋め込んだケース
func Benchmark_CaseForUsingGoEmbed(b *testing.B) {
	cfg := &Config{}
	json.Unmarshal(configByte, cfg)
}

// os パッケージを使ってファイルを読み込むケース
func Benchmark_CaseForUsingOSPackage(b *testing.B) {
	buf, err := os.ReadFile("config.json")
	if err != nil {
		panic(err)
	}
	cfg := &Config{}
	json.Unmarshal(buf, cfg)
}

ベンチマークの計測結果は以下の通りとなりました。

go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: project
cpu: VirtualApple @ 2.50GHz
// embed パッケージを利用                            ↓ 実行回数                 ↓ 実行速度
Benchmark_CaseForUsingGoEmbed-8         1000000000               0.0000109 ns/op               0 B/op          0 allocs/op
// os パッケージでファイルを読込
Benchmark_CaseForUsingOSPackage-8       1000000000               0.0001525 ns/op               0 B/op          0 allocs/op
PASS
ok      project    0.379s

ファイルの読み込みが発生しない分だけ embed パッケージを使って読み込んだ方が早く動作するようです。
HTML やアセットファイルを配信するようなケースではリクエストの度にファイルを読み込むことになるため、embed パッケージを使うことでパフォーマンスの向上が見込めるかもしれません。

最後に

今回は embed パッケージについて紹介させていただきました。
Go のコードとファイルを分割して管理していたところを embed パッケージを使うことで一つにまとめることが可能になりました。
今後も Go のコードを書く際は embed パッケージで解決できる部分は積極的に使っていこうと思います。


この記事が気に入ったらサポートをしてみませんか?