Go v1.16に追加されたembedを試してみる

3 min読了の目安(約3500字TECH技術記事

Go v1.16がリリースされましたね!

https://golang.org/doc/go1.16

追加された機能がいろいろありますが、今回はembedを試してみます!

https://future-architect.github.io/articles/20210207/

embedって何?

  • embedとは埋め込みという意味
  • ファイルの埋め込みができるようになった
  • 今までは、ファイルを読み込みは osio/ioutil で行っていた
    • io/ioutilはv1.16からdeprecatedになった

embed何がいいの?

  • ファイルの読み込みが楽になった
  • ビルド時に埋め込んだファイルはバイナリに含まれる
    • Dockerfileとか書くの楽になった

使い方

この記事のサンプルコードはこちらにあります

https://github.com/nekoshita/golang-embed-example

単一のファイルを読み込む①

sample_bytes.txt

Sample Bytes!

sample_string.txt

Sample String!

main.go

package main

import (
	_ "embed"
	"fmt"
)

//go:embed sample_bytes.txt
var sampleBytes []byte

//go:embed sample_string.txt
var sampleString string

func main() {
	fmt.Printf("%s\n", sampleBytes)
	fmt.Printf("%s\n", sampleString)
}

こんな感じでファイルの埋め込みができます。
ディレクティブで埋め込める変数の型は、string、[]byte、embed.FSの3種類です。
埋め込みは関数の内部など閉じたスコープで行うことができません。

単一のファイルを読み込む②

package main

import (
	"embed"
	"fmt"
	"io/fs"
)

//go:embed sample_bytes.txt sample_string.txt
var static embed.FS

func main() {
	b, err := static.ReadFile("sample_bytes.txt")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", string(b))

	b2, err := fs.ReadFile(static, "sample_string.txt")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", string(b2))
}

このように embedパッケージを使ってファイルを読み込むことも可能です。

モックサーバー立てる

todo.json

[
  {
    "id": "a",
    "content": "abc"
  },
  {
    "id": "b",
    "content": "edf"
  }
]

user.json

{
  "id": "abc123",
  "user": {
    "name": "taro",
    "age": 20
  }
}

main.go

package main

import (
	"embed"
	"log"
	"net/http"
)

//go:embed todo.json user.json
var responseJson embed.FS

func main() {
	http.Handle("/", http.FileServer(http.FS(responseJson)))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

net/httpパッケージで提供されているファイルシステムとも互換性があります。
http://localhost:8080/user.json にアクセスするとjsonを取得できます。

ディレクトリごと読み込む

sql/sample1.sql

SELECT *
FROM users

sql/sample2.sql

SELECT *
FROM todos

main.go

package main

import (
	"embed"
	"fmt"
)

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

func main() {
	sample1, err := sql.ReadFile("sql/sample1.sql")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", string(sample1))

	sample2, err := sql.ReadFile("sql/sample2.sql")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", string(sample2))
}

ワイルドカードを使ってディレクトリごと読み込むことも可能です。
また、../のように親ファイルまで遡って読み込みはできません。

ウェブサイト

ディレクトリ構成

static
 L public
    L favicon.ico
    L index.css
    L index.html

main.go

package main

import (
	"embed"
	"io/fs"
	"log"
	"net/http"
)

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

func main() {
	public, err := fs.Sub(static, "static/public")
	if err != nil {
		panic(err)
	}
	http.Handle("/", http.FileServer(http.FS(public)))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

こんな感じでワイルドカードを使って階層を掘り下げる形でも使えます。
http://localhost:8080/にアクセスするとWEBページがちゃんと表示されます。

ビルドするとどうなる?

ビルドするとファイルは埋め込まれます。
つまり、今までのように実行用のバイナリとアセットファイルに別れることはなくなります。

Dockerfileを書いたり、デプロイしたりするのがちょっと楽になりますね!

まとめ

  • ファイルの読み込みが楽になった
  • ビルド時に埋め込んだファイルはバイナリに含まれる
    • Dockerfileとか書くの楽になった

僕が使うなら

僕が今いる現場でクエリを.sql形式のファイルで書かないと嫌だという方がいて、sqlファイルがたくさんあり、それを ioutilで読み込む実装にしているところがあります。
その部分をioutilはdeprecatedになりましたし、embedのが楽にかけますし、Dockerfileもシンプルになるので、そこを置き換えたと思います!

参考にした記事

https://golang.org/pkg/embed/
https://future-architect.github.io/articles/20210208/

https://zenn.dev/koya_iwamura/articles/53a4469271022e