Go1.16新機能embedを使ってメッセージが定義されたファイルを読み込む

# はじめに
## Go1.16リリース
Go1.16が2021年2月にリリースされましたね！

- [Go 1.16 Release Notes](https://golang.org/doc/go1.16)
- [Go 1.16連載が始まります | フューチャー技術ブログ](https://future-architect.github.io/articles/20210207/)

様々なアップデートがありましたが、主要でわかりやすい変更点は以下の通りでしょうか。

- M1 Macに対応
- 処理速度やメモリ使用量の改善
- `go:embed`の追加

> Go 1.16 adds support of 64-bit ARM architecture on macOS (also known as Apple Silicon) with GOOS=darwin, GOARCH=arm64.

> For a representative set of large Go programs, linking is 20-25% faster than 1.15 and requires 5-15% less memory on average for linux/amd64, with larger improvements for other architectures and OSes. Most binaries are also smaller as a result of more aggressive symbol pruning.

> The go command now supports including static files and file trees as part of the final executable, using the new //go:embed directive.

【引用】[Go 1.16 Release Notes](https://golang.org/doc/go1.16)

## この記事でやること
今回はこの中でも`go:embed`を使って、「メッセージ一覧が定義されたjsonファイルを読み込み、そのメッセージを取得する」というモジュールの実装を解説したいと思います！

# 完成形
完成形だけ知りたい方向けにこちらにコードを貼っておきます。

```sh:ディレクトリ構成
(YOUR_DIRECTORY)
├─ message/
|   ├─ message.go
|   └─ messages.json
└─ main.go
```

```json:messages.json
{
    "key1": "go:embed使ってみたよ！",
}
```

```go:message.go
package message

import (
	// import embed
	_ "embed"
	"encoding/json"

	"github.com/labstack/gommon/log"
)

//go:embed messages.json
var msgJSON []byte

var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
	if err := json.Unmarshal(msgJSON, &msgs); err != nil {
		panic("Cannot read messages.json")
	}
}

// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
	msg, exists := msgs[key]
	if !exists {
		log.Errorf("Cannnot find this message key: %s", key)
	}
	return msg
}
```

```go:main.go
func main() {
	// 一度だけ呼べばOK
	message.Read()

	// メッセージを取り出す
	fmt.Println(message.Get("key1"))
}
```

```sh:実行結果
$ go run .
go:embed使ってみたよ！
```

# 解説
## embedとは
`embed`は埋め込みという意味です。
名前の通り、ファイルを変数に埋め込むことができます。

ファイルの読み込み自体は今までもできましたが、複数の書き方があったり、やや冗長な感じがありました。

【参考】[[Golang] ファイル読み込みサンプル](https://qiita.com/tchnkmr/items/b686adc4a7e144d48755)

しかし、`embed`を使うことにより、上記`message.go`のようにものの数行で書けるようになりました。

また、`embed`で読み込んだファイルはビルドされたバイナリにも埋め込まれます。
これには以下のようなメリットがあります。

> これはGoの利点の一つである、単一の実行ファイルとしてビルドできることで、展開先の依存関係をシンプルに保つことができるという利点を強力に後押しします。設定ファイルや各種アセットをビルドに含めることで、バージョン管理やリリース作業を一層シンプルに整理できることが期待できます。

> 先ほど紹介した簡易WEBサーバーで例えると、WEBサーバーとコンテンツとなるHTML、CSS、Javascriptが分離している場合、ローカル環境で動いたものを実際の環境にデプロイする場合、実行バイナリと各種アセットをデプロイ対象の環境で適宜整理する必要があります。

> これらを全て単一のバイナリに含めることができた場合、作業は実行バイナリを一つコピーして起動するだけになります。
新しいサーバーにデプロイする際の運用フローの整備や、プロダクション向けの構成でコンテナを構築するDockerfileを書いていく事を考えると、go:embedで極限まで簡略化できる部分が想像できるかもしれません。

【引用】[Go 1.16からリリースされたgo:embedとは](https://future-architect.github.io/articles/20210208/)

## コード解説
`message.go`の解説をしていきます。

```go
import (
	// import embed
	_ "embed"
)
```

`embed`パッケージをimportします。

コード中で`embed.~~`のような使い方をしないので、ブランクインポートをします。（正確なニュアンスではないですが）

```go
//go:embed messages.json
var msgJSON []byte
```

ここが`embed`のキモですね。
`message.go`と同じディレクトリにある`messages.json`を変数`msgJSON`に埋め込みます。
もちろん必ず同じディレクトリに配置する必要はなく、相対パスで指定することも可能です。

```go
var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
	if err := json.Unmarshal(msgJSON, &msgs); err != nil {
		panic("Cannot read messages.json")
	}
}
```

よく見かけるコードですね。
mapの`msgs`に`msgJSON`を代入します。

【補足】mapで定義した理由
> 構造体を定義しておく手法が一般的かと思いますが、`messages.json`の中身が増えるたびに構造体のプロパティを追加していくのが面倒だったのでmapに代入するようにしました。
今回はjsonファイルに文字列しか登場しておらず、`map[string]string`で済むのも、mapで定義した理由の1つです。
数字など他の型も考慮する必要がある場合、`map[string]interface{}`にする必要があり、型安全ではなくなってしまいます。
このように複数の型を扱う場合や、ちゃんと型で守りたい場合は、構造体を使った方が良いと思います。

> 【参考】[Go言語でJSONを扱う](https://qiita.com/nayuneko/items/2ec20ba69804e8bf7ca3)



```go
// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
	msg, exists := msgs[key]
	if !exists {
		log.Errorf("Cannnot find this message key: %s", key)
	}
	return msg
}
```
ここはただmapからkeyでvalueを取り出しているだけです。
jsonファイルに存在しないkeyが引数で渡ってきた場合log出力するようにしています。
ちなみに存在しない場合、`msg`は空文字となります。

# さいごに
Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">2月にリリースされたGo1.16の新機能「embed」を使ってファイルの読み込みをしてみました！<br>メッセージが定義されたjsonファイルを読み込んでみたのですが、結構いい感じに使えて便利でした<br>Go1.16の新機能が気になる方やファイル読み込みに悩んでいる方は読んでみてください😇<a href="https://t.co/SIp6KqJspl">https://t.co/SIp6KqJspl</a></p>&mdash; やぎぬ😇行動力エンジニア (@yagi_eng) <a href="https://twitter.com/yagi_eng/status/1371291179396952067?ref_src=twsrc%5Etfw">March 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

また、BOT開発を通じてGoとLINE BOTにまとめて入門する記事をZennに掲載していますので、良かったらそちらもご覧ください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ZennでGoとLINE BOTの記事を書いてみました<br><br>⬇️BOT開発を通じてGoとLINE BOTにまとめて入門する<a href="https://t.co/QqsEESJMKa">https://t.co/QqsEESJMKa</a><br><br>5ステップに分け、「Hello Worldから始めて、飲食店検索ができるLINE BOTの実装まで」を解説しています<br>GoやLINE BOTに興味のある人は是非読んでみてください😇<br>24,000字超え😇</p>&mdash; やぎぬ😇行動力エンジニア (@yagi_eng) <a href="https://twitter.com/yagi_eng/status/1325027213784326152?ref_src=twsrc%5Etfw">November 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 参考
[Go 1.16からリリースされたgo:embedとは](https://future-architect.github.io/articles/20210208/)