Go で web server を立てる際に、環境(staging とか production とか)ごとの設定値をどういうふうに管理し、アクセス可能にするかのメモ。
外からの値の受け取り方としては環境変数から取るか、コマンドラインフラグで渡すかの2択だけど、どちらを取ったとしてもアプリケーションコードのロジック中からその値をどのように参照するかという話がある。
毎回 os.Getenv
とか書きたくないし、このアプリケーションにどのような設定を入れられるのかがわかりづらいので、どこかで一元管理したい。
そこで、以下のように設定管理用の myconfig
というパッケージを作っておいて、そこに集約させてみる。
設定値を持った構造体をパッケージグローバル変数に突っ込んでおいて、その構造体経由で設定値を参照できるようにする。
myconfig.go
package myconfig
import (
"github.com/caarlos0/env/v6"
/*...以下省略*/
)
var cfg config
type config struct {
DBName string `env:"DB_NAME" envDefault:"hoge_test"`
DBUser string `env:"DB_USER" envDefault:"root"`
DBPass string `env:"DB_PASS" envDefault:"password"`
DBHost string `env:"DB_HOST" envDefault:"localhost"`
DBPort int `env:"DB_PORT" envDefault:"3306"`
}
// パッケージ初期化時に一度だけ環境変数をロードし、cfg 変数にセットします
func init() {
if err := env.Parse(&cfg); err != nil {
panic(fmt.Sprintf("failed to setup config, error: %s", err))
}
}
func DBName() string {
return cfg.DBName
}
func DBUser() string {
return cfg.DBUser
}
func DBPass() string {
return cfg.DBPass
}
func DBHost() string {
return cfg.DBHost
}
func DBPort() int {
return cfg.DBPort
}
値を使う側はこんな感じで、myconfig
パッケージに用意されたゲッター経由でアプリケーションのどこからでもアクセスできる。
main.go
func main() {
fmt.Println(myconfig.DBName())
}
値は全て環境変数から取る前提である。
一応ポイントというか解説というか
- 環境変数から値を取り出す処理は ライブラリを使って楽している
- ライブラリに依存したくない場合は、自分で書いても全然良い
- フィールドを export しているのはライブラリから参照させるためなので、自分で書く場合はフィールドも unexported にして良い
- init 関数で初期化し、環境変数から取得した値をセットした構造体を作って、グローバル変数に入れておく
- init 関数ではなく自前の initialize 関数を用意して明示的に呼ぶ形でも良い
- その場合、シングルトンパターンで
config
構造体の初期化は一度のみに制限するなどの工夫をした方がいいかも
- その場合、シングルトンパターンで
- init 関数ではなく自前の initialize 関数を用意して明示的に呼ぶ形でも良い
- ゲッターのみを提供することで、不用意に設定値を変えてしまうリスクをなくす
-
config
構造体のフィールドで設定値を一元管理しているので、どのような値を設定できるのかの確認がしやすい
ローカル開発用に .env ファイルなどを使いたい場合
こんな感じで、.env と併用させることもできる。
.env ファイルがあるときはそれを読み込んで値をオーバーライドするようにしておく。
(.env ファイルへのパスは適宜合わせる)
myconfig.go
package myconfig
import (
"github.com/joho/godotenv"
/*...以下省略*/
)
var cfg config
type config struct {/*...省略*/}
// パッケージ初期化時に一度だけ環境変数をロードし、cfg 変数にセットします
func init() {
// 開発環境以外では .env を使用しないのでここのエラーは無視します
_, b, _, _ := runtime.Caller(0)
_ = godotenv.Load(filepath.Join(filepath.Dir(b), "../.env_dev"))
if err := env.Parse(&cfg); err != nil {
panic(fmt.Sprintf("failed to setup config, error: %s", err))
}
}
flag と環境変数を併存させたい場合
あまりないかもだが、flag と環境変数を併存させてみる。この場合、両方設定されているときにどちらの値を優先的に評価するかを決めておく必要がある。
この例では、flag の値を優先している。
myconfig.go
type config struct {
WriterDBName *string
}
func init() {
cfg = config {
WriterDBName: flag.String("DB_NAME", getEnvString("DB_NAME", "hoge_test"), "db name")
/*...省略*/
}
}
func getEnvString(envKey string, defaultVal string) string {
if envVal, isSet := os.LookupEnv(envKey); isSet {
return envVal
}
return defaultVal
}
-
flag.String
の第二引数が flag が設定されていない時のデフォルト値- その値を環境変数から値が取れればその値を、取れなければ引数に与えたデフォルト値を返すようにしている
- flag > 環境変数 > デフォルト値、という順で評価されるようにしている