yoshikipom Tech Blog

Goアプリケーションでシンプルな設定値管理を実現する

はじめに

あるGoアプリケーションでぼちぼち設定値が増えてきたのでリファクタリングしたのでメモ。以下が達成したかったこと。

  • 設定値の一覧性
  • 読み出し時のシンプルさ
  • 環境変数での設定値インジェクション

動的な値の更新は対応しない。今回の要件では再デプロイでの設定値更新で十分であったため。

全体像

全体像 https://github.com/yoshikipom/go/tree/main/config

.
├── config
│   └── config.yml // 設定ファイル
├── go.mod
├── go.sum
├── internal
│   └── config
│       └── config.go // 設定ファイルと環境変数からgoの構造体へのマッピング
└── main.go // テスト用

使い方

config.yml

port: 8080
key: value

アプリケーション内で一度だけ Initialize関数を呼ぶ。 エラーが出る可能性があるのはInitializeのみにできるのであとは使いたい場所でGetConfigを呼ぶだけ。 使っているライブラリはconfigパッケージに隠蔽されているのでライブラリの変更は安全に行える。 単体テストでテスト用のファイルを渡したくなることがあったので、 Initialize関数で設定ファイルのパスを渡すことにした。

main.go

package main

import (
    "fmt"

    "github.com/yoshikipom/go/config/internal/config"
)

func main() {
    // call only once
    err := config.Initialize("./config/config.yml")
    if err != nil {
        panic(err)
    }

    // you can get config anywhere
    c := config.GetConfig()
    fmt.Printf("%+v", c)
}

実行例

# config.ymlの値が読めている
yoshiki@yoshiki-mbp:go/config ‹main*›$ go run "/Users/yoshiki/work/study/go/config/main.go"              
&{Port:8080 Key:value}%

# 環境変数で渡した"new_value"が読み込めている。(key= でなく KEY= の理由は後述。)
yoshiki@yoshiki-mbp:go/config ‹main*›$ KEY=new_value go run "/Users/yoshiki/work/study/go/config/main.go"
&{Port:8080 Key:new_value}%  

config.goの実装

package config

import (
    "github.com/spf13/viper"
)

var config *MyConfig

type MyConfig struct {
    Port int    `mapstructure:"port"`
    Key  string `mapstructure:"key"`
}

func Initialize(fileName string) error {
    viper.SetConfigFile(fileName)
    if err := viper.ReadInConfig(); err != nil {
        return err
    }

    viper.AutomaticEnv()

    c := &MyConfig{}
    if err := viper.Unmarshal(c); err != nil {
        return err
    }
    config = c
    return nil
}

func GetConfig() *MyConfig {
    if config == nil {
        panic("config is not initialized.")
    }
    return config
}
  • Viper というライブラリを利用

おわりに

使いやすい設定値管理ができてよかった。Viperはシンプルかつ高機能で良い。