🐍

spf13/viper について - Golang, Go言語

に公開

Viper とは

Goで書かれた設定管理用のパッケージ。
環境変数, JSON, YAML, etc. から行った設定を Viperを通じて呼び出せる。

https://github.com/spf13/viper/tree/master?tab=readme-ov-file#what-is-viper

reading from JSON, TOML, YAML, HCL, envfile and Java properties config files
live watching and re-reading of config files (optional)
reading from environment variables
reading from remote config systems (etcd or Consul), and watching changes
reading from command line flags
reading from buffer
setting explicit values
Viper can be thought of as a registry for all of your applications configuration needs.

大まかな仕組み


Viper はさまざまなソースから key:value の設定を取得し、内部では v.config, v.env などの map に格納する。

https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L200-L207

	v.config = make(map[string]any)
	v.parents = []string{}
	v.override = make(map[string]any)
	v.defaults = make(map[string]any)
	v.kvstore = make(map[string]any)
	v.pflags = make(map[string]FlagValue)
	v.env = make(map[string][]string)
	v.aliases = make(map[string]string)

yaml からの設定は v.config, 環境変数は v.env など、リソースによって異なる map (≒ 設定の入れ物) に保存される。


設定の取り出し時には、map に格納された値が find() によって取り出される。

https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L1144

find() は v.config, v.env など、複数の map のいずれかを参照し、値を取り出す。
find() が参照する map の優先順位は、以下になっているようだ。

// 1. overrides
// 2. flags
// 3. env. variables
// 4. config file
// 5. key/value store
// 6. defaults


実際のコードからは、 v.override の値が優先されて返されることが分かる。

https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L1163-L1166

	val = v.searchMap(v.override, path)
	if val != nil {
		return val
	}

つまり、最初に v.override を見て、
v.override から値が取れなければ v.pflags,
それもダメなら v.env, ... といった具合である。

yaml から設定し、その設定を呼び出す場合

以下の例では、yamlから key:value を保存した後、viper.Get()によって保存されたvalueを呼び出している。

サンプルコード(公式repoから引用):

https://github.com/spf13/viper/tree/master?tab=readme-ov-file#reading-config-from-ioreader


// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))  // step 1

viper.Get("name")  // step 2


viper.Get("name") の結果、"steve" が返される。

上記 step 1, step 2 について、何が行われているのか詳しく見ていくことにする。

step 1: 設定の取り込み

viper.ReadConfig(bytes.NewBuffer(yamlExample))

bytes.NewBuffer(yamlExample)

bytes.NewBuffer(yamlExample) は、*bytes.Bufferを返す。
https://pkg.go.dev/bytes#NewBuffer

bytes.Bufferは io.Reader を実装するので、いわゆる os.File みたいなものだと認識している。

次に、ReadConfig() が何をしているか気になる。

ReadConfig()

in と v.configを unmarshalReader() に渡す。
https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L1537-L1544

func (v *Viper) ReadConfig(in io.Reader) error {
	if v.configType == "" {
		return errors.New("cannot decode configuration: config type is not set")
	}

	v.config = make(map[string]any)
	return v.unmarshalReader(in, v.config)
}

in には yaml から作られた bytes.Buffer が入っている。
v.configは map[string]any, つまり設定の入れ物である。
これらを v.unmarshalReader() に渡す。

unmarshalReader()

https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L1664

func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error {
	buf := new(bytes.Buffer)
	buf.ReadFrom(in)

...
	decoder, err := v.decoderRegistry.Decoder(format)
	if err != nil {
		return ConfigParseError{err}
	}

	err = decoder.Decode(buf.Bytes(), c)
	if err != nil {
		return ConfigParseError{err}
	}

bytes.Buffer から貰った設定を c (これは v.config) にブチ込んでいるようである。

結局、ReadConfig()がやっていることは yamlの内容を v.configに格納。

step 2: 値の呼び出し

viper.Get("name")

Get()

find() を呼び出している。
https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L711

func (v *Viper) Get(key string) any {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, true)
	if val == nil {
		return nil
	}

find()

key から value を取ってくる機能を持つ。

前述の通り、find() は、複数のmap(v.config, v.env, ...) のいずれかから値を取得する。

v.config から値を取得する部分は以下。

https://github.com/spf13/viper/blob/9c07e0f0633cae75a540fc606c6ebb3bece61826/viper.go#L1230-L1237

	path = strings.Split(lcaseKey, v.keyDelim)
    ...

	// Config file next
	val = v.searchIndexableWithPathPrefixes(v.config, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
		return nil
	}

まとめ

  • Viper は複数のソースから設定を取得する。ソースの種類によって、 v.config, v.env, v.override などの異なる map に設定を保存する。
  • 設定の呼び出し時には、find() メソッドによって、複数の map のいずれかから値が取得される。どの map から値を取得するかには優先順位がある。

Discussion