はじめに
本記事は、Goでアプリケーションで使用する設定値を外部ファイルから取得する方法についてのメモです。
設定ファイルのフォーマット
設定ファイルを定義するフォーマットとして、メジャーなものは以下のとおりです。
フォーマット | 特徴 |
---|---|
ini | Windowsで主に使用。明確な仕様がないため実装に差異がある。 |
json | 対応言語が多い。拡張性もあるが、コメントが書けない。RFC4627に仕様が明記 |
yaml | JSON相当の汎用性があり、コメントが書ける。若干の学習が必要。 |
toml | 新しいフォーマットでライブラリによる品質に差異がある。 |
参考: みんなのGo言語 改訂2版 ISBN978-4-297-10727-7 |
どのフォーマットを使うかは好きなもので良いですが、オススメできそうなのはjsonかyamlです。
今回は、以前の記事でデータベースの接続情報を以下のようにハードコーディングしていて、それらの設定を外部にもっていくことを考えています。この設定は、基本的にはアプリケーションの管理者だけが知っていれば良い情報です。そのためコメントが特に必要というわけではないので、jsonを使います。
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として保存します。
{
"dbDriver" : "postgres",
"dsn" : "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable"
}
次に、jsonから読み込んだ情報を格納するための構造体を作ります。フィールド毎にjsonの対応するフィールドをjson:'dbDriver'
のようにそれぞれ書いてください。
type config struct {
DbDriver string `json:'dbDriver'`
Dsn string `json:'dsn'`
}
注意点としては、構造体のフィールド名の1文字目は必ず大文字にする必要があります。
そうしないとjsonから読み込んだ設定値が期待するフィールドに設定されません。私は地味にハマりました。
設定ファイルの読み込みは以下のように、os.Openで読み込んだファイルをjson.NewDecoderのパラメータに指定して、Decoderを取得します。取得したDecoderのDecodeメソッドを使って、あらかじめ用意しておいたconfig構造体に設定しています。
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版には、設定ファイルのマルチプラットフォーム対応や設定ファイルの配置場所などについての記載もあります。
非常にわかり易くまとまっていますので、そちらの内容もオススメです。
設定ファイル読み込み実験(設定ファイルが見つからない場合)
設定ファイル名をconfig.jsonからconfig1.jsonとして、存在しないファイル名を指定してみました。
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を追加してみました。
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側に構造体には存在しないフィールドを追加してみます。
{
"dbDriver" : "postgres",
"dsn" : "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable",
"dummy3" : "test"
}
こちらもjsonを読み込んでみましたが、エラーにはなりません。
設定ファイル読み込み実験(入れ子)
アプリケーションの規模が大きくなると設定ファイルにフラットにプロパティを列挙していくと、見通しが悪くなったり何かと不都合があります。jsonフォーマットは、階層化して情報を整理できるので、以下のようにデータベースの接続情報を設定する要素dbを作ってみます。
{
"db" :
{
"dbDriver" : "postgres",
"dsn" : "host=127.0.0.1 port=15432 user=postgres password=!qaz2wsx dbname=go_tutorial01 sslmode=disable"
}
}
このようなjsonファイルを読み込む場合、構造体も、構造体の中に構造体を定義して入れ子状態にする必要があります。
type db struct {
DbDriver string `json:'dbDriver'`
Dsn string `json:'dsn'`
}
type config struct {
Db db `json:'db'`
}
設定ファイルの読み込み(yaml)
設定ファイルが大きくなると、コメントを書きたくなるので、yamlについても調べてみました。
jsonのものをyamlで記述すると以下のようになります。
# 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として読み込んでも動きます。
{
# 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"のようにして囲んでください。
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と同様で以下のようにするだけです。
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
}
参考文献
この記事は以下の情報を参考にして執筆しました。