私が歌川です

@utgwkk が書いている

github.com/caarlos0/env を使った設定値に必要な環境変数名を列挙する

アプリケーションの設定値を環境変数から注入していますか?

Goの場合、structに設定値を集約するのには GitHub - caarlos0/env: A simple and zero-dependencies library to parse environment variables into structs. などのライブラリが使えると思います。一方でこういうライブラリでstructをネストさせていくと、結局どの環境変数が必要なのかざっと眺めるのが難しいです。

type Config struct {
    Port        int          `env:"PORT" envDefault:"8080"`
    DatabaseDSN string       `env:"DATABASE_DSN,required"` // 必須
    Nested      NestedConfig `envPrefix:"NESTED_"`
}

type NestedConfig struct {
    DSN string `env:"DSN,required"` // 必須
}

↑これぐらいならいいけど実際にはもっと設定項目があるはず。NESTED_DSN という環境変数をセットする必要があるのだけど、↑のような実装だとアプリケーションコードを NESTED_DSN でgrepしてもヒットしない!!

ということでリフレクションで解決しましょう。

package main

import (
    "fmt"
    "reflect"
    "strings"

    "golang.org/x/exp/slices"
)

type Config struct {
    Port        int          `env:"PORT" envDefault:"8080"`
    DatabaseDSN string       `env:"DATABASE_DSN,required"`
    Nested      NestedConfig `envPrefix:"NESTED_"`
}

type NestedConfig struct {
    DSN string `env:"DSN"`
}

func main() {
    // アプリケーションで使う設定値用のstructを渡す
    cfg := Config{}
    envVars := aggregateEnvVars(cfg, "", "")
    for _, ev := range envVars {
        fmt.Printf("%s: %s", ev.Name, ev.FieldPath)
        if ev.Required {
            fmt.Print(" (required)")
        }
        fmt.Println()
    }
}

type envVar struct {
    FieldPath string
    Name      string
    Required  bool
}

func aggregateEnvVars(cfg any, envPrefix, fieldPath string) []envVar {
    st := reflect.ValueOf(cfg)
    var varNames []envVar

    for i := 0; i < st.NumField(); i++ {
        field := st.Field(i)

        // skip unexported field
        if !field.CanInterface() {
            continue
        }

        fieldTy := st.Type().Field(i)

        // envPrefix tagがあるならstruct型のフィールドなので再帰的にたどる
        newEnvPrefix := fieldTy.Tag.Get("envPrefix")
        if newEnvPrefix != "" {
            varNames = append(varNames, aggregateEnvVars(field.Interface(), envPrefix+newEnvPrefix, fieldPath+"."+fieldTy.Name)...)
            continue
        }

        // env:"AAA,required"
        envTag := fieldTy.Tag.Get("env")
        if envTag == "" {
            continue
        }
        xs := strings.Split(envTag, ",")
        varName := xs[0]
        required := slices.Contains(xs, "required")

        varNames = append(varNames, envVar{
            FieldPath: fieldPath+"."+fieldTy.Name,
            Name:     envPrefix + varName,
            Required: required,
        })
    }

    return varNames
}

実行すると以下のような出力が得られます。どのフィールドの値がどの環境変数に対応するのか一目で分かって便利ですね。structのフィールドにコメントを書いていたらそれも出力されてほしいけど、それをやるには静的解析が必要な予感がします。

PORT: .Port
DATABASE_DSN: .DatabaseDSN (required)
NESTED_DSN: .Nested.DSN