Goで設定ファイルを扱う

# はじめに
本記事は、Goでアプリケーションで使用する設定値を外部ファイルから取得する方法についてのメモです。

## 設定ファイルのフォーマット
設定ファイルを定義するフォーマットとして、メジャーなものは以下のとおりです。

|フォーマット|特徴|
|:--|:--|
|ini|Windowsで主に使用。明確な仕様がないため実装に差異がある。|
|json|対応言語が多い。拡張性もあるが、コメントが書けない。[RFC4627](https://www.ietf.org/rfc/rfc4627.txt)に仕様が明記|
|yaml|JSON相当の汎用性があり、コメントが書ける。若干の学習が必要。|
|toml|新しいフォーマットでライブラリによる品質に差異がある。|
参考: [みんなのGo言語 改訂2版 ISBN978-4-297-10727-7](https://gihyo.jp/book/2019/978-4-297-10727-7)

どのフォーマットを使うかは好きなもので良いですが、オススメできそうなのはjsonかyamlです。

今回は、[以前の記事](https://qiita.com/taka23kz/items/cb7ae9ac6cf343b3dec2)でデータベースの接続情報を以下のようにハードコーディングしていて、それらの設定を外部にもっていくことを考えています。この設定は、基本的にはアプリケーションの管理者だけが知っていれば良い情報です。そのためコメントが特に必要というわけではないので、jsonを使います。

```go
dbDriver := "postgres"
dsn := "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable"
db, err := setupDB(dbDriver, dsn)
```

## 設定ファイルの読み込み(json)
設定ファイルの情報は、DBドライバ(dbDriver)と、DBの接続文字列(dsn)にします。これをjsonで書くと以下のようになります。これをconfig.jsonとして保存します。

```json:config.json
{
    "dbDriver" : "postgres",
    "dsn" : "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable"
}
```

次に、jsonから読み込んだ情報を格納するための構造体を作ります。フィールド毎にjsonの対応するフィールドを`json:'dbDriver'`のようにそれぞれ書いてください。


```go
type config struct {
	DbDriver string `json:'dbDriver'`
	Dsn      string `json:'dsn'`
}
```

注意点としては、構造体の**フィールド名の1文字目は必ず大文字にする**必要があります。
そうしないとjsonから読み込んだ設定値が期待するフィールドに設定されません。私は地味にハマりました。

設定ファイルの読み込みは以下のように、[os.Open](https://golang.org/pkg/os/#Open)で読み込んだファイルを[json.NewDecoder](https://golang.org/pkg/encoding/json/#NewDecoder)のパラメータに指定して、Decoderを取得します。取得したDecoderのDecodeメソッドを使って、あらかじめ用意しておいたconfig構造体に設定しています。

```go
func loadConfig() (*config, error) {
	f, err := os.Open("config.json")
	if err != nil {
		log.Fatal("loadConfig os.Open err:", err)
		return nil, err
	}
	defer f.Close()

	var cfg config
	err = json.NewDecoder(f).Decode(&cfg)
	return &cfg, err
}
```

参考にさせていただいた[みんなのGo言語 改訂2版](https://gihyo.jp/book/2019/978-4-297-10727-7)には、設定ファイルのマルチプラットフォーム対応や設定ファイルの配置場所などについての記載もあります。
非常にわかり易くまとまっていますので、そちらの内容もオススメです。

### 設定ファイル読み込み実験(設定ファイルが見つからない場合)
設定ファイル名をconfig.jsonからconfig1.jsonとして、存在しないファイル名を指定してみました。

```go
	f, err := os.Open("config1.json")
	if err != nil {
		log.Fatal("loadConfig os.Open err:", err)
		return nil, err
	}
```

この場合は、以下のようなエラーが表示されます。

```
2020/04/04 16:44:44 loadConfig os.Open err:open config1.json: The system cannot find the file specified.
```

### 設定ファイル読み込み実験(構造体とjsonファイルの内容が不一致の場合)
構造体にjsonファイルに存在しないフィールドとしてDummy1とDummy2を追加してみました。

```go
type config struct {
	DbDriver string `json:'dbDriver'`
	Dsn      string `json:'dsn'`
	Dummy1   int    `json:'dummy1'`
	Dummy2   string `json:'dummy2'`
}
```

jsonを読み込んで内容を表示してみた結果は以下のとおりです。json側にフィールドが無くても読込自体はエラーとならずに、構造体のそれぞれのフィールドには、型の初期値が設定されるみたいです。

```
dummy1: 0
dummy2: 
```

今度は逆に、json側に構造体には存在しないフィールドを追加してみます。

```json:config.json
{
    "dbDriver" : "postgres",
    "dsn" : "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable",
    "dummy3" : "test"
}
```

こちらもjsonを読み込んでみましたが、エラーにはなりません。

### 設定ファイル読み込み実験(入れ子)
アプリケーションの規模が大きくなると設定ファイルにフラットにプロパティを列挙していくと、見通しが悪くなったり何かと不都合があります。jsonフォーマットは、階層化して情報を整理できるので、以下のようにデータベースの接続情報を設定する要素dbを作ってみます。

```json:config.json
{
    "db" : 
    {
        "dbDriver" : "postgres",
         "dsn" : "host=127.0.0.1 port=15432 user=postgres password=!qaz2wsx dbname=go_tutorial01 sslmode=disable"
    }
}
```

このようなjsonファイルを読み込む場合、構造体も、構造体の中に構造体を定義して入れ子状態にする必要があります。

```go
type db struct {
	DbDriver string `json:'dbDriver'`
	Dsn      string `json:'dsn'`
}
type config struct {
	Db db `json:'db'`
}
```

## 設定ファイルの読み込み(yaml)
設定ファイルが大きくなると、コメントを書きたくなるので、yamlについても調べてみました。
jsonのものをyamlで記述すると以下のようになります。

```yaml:config.yml
# dbの接続情報に関する設定
db :  
 # dbドライバ
 dbDriver : postgres
 # db接続文字列
 dsn : host=127.0.0.1 port=15432 user=postgres password=!qaz2wsx dbname=go_tutorial01 sslmode=disable
```

実際には、以下のようにjsonにコメントを付けてyamlとして読み込んでも動きます。

```yaml:config2.yml
{
    # dbの接続情報に関する設定
    "db" : 
    {
        # dbドライバ
        "dbDriver" : "postgres",
        # db接続文字列
         "dsn" : "host=127.0.0.1 port=15432 user=postgres password=!qaz2wsx dbname=go_tutorial01 sslmode=disable"
    }
}
```

yamlを使うために「github.com/go-yaml/yaml」をgo getで取得します。

```
go get "github.com/go-yaml/yaml"
```

構造体のフィールドをyamlのフィールドに対応させるために、yaml:"dbDriver"のように書いていきます。
このとき、注意点としてdbDriverの部分を'dbDriver'のようにシングルクォーテーションで囲むのでは無く、ダブルクォーテーションで"dbDriver"のようにして囲んでください。

```go
type db struct {
	DbDriver string `json:"dbDriver" yaml:"dbDriver"`
	Dsn      string `json:"dsn" yaml:"dsn"`
}

type config struct {
	Db db `json:"db" yaml:"db"`
}

```

読み込みはjsonと同様で以下のようにするだけです。

```go
func loadConfigForYaml() (*config, error) {
	f, err := os.Open("config.yml")
	if err != nil {
		log.Fatal("loadConfigForYaml os.Open err:", err)
		return nil, err
	}
	defer f.Close()

	var cfg config
	err = yaml.NewDecoder(f).Decode(&cfg)
	return &cfg, err
}
```
## 参考文献
この記事は以下の情報を参考にして執筆しました。

- [Package encoding/json](https://golang.org/pkg/encoding/json/)
- [みんなのGo言語 改訂2版 ISBN978-4-297-10727-7](https://gihyo.jp/book/2019/978-4-297-10727-7)