CodeSampleX

サンプル

github.com/spf13/viper v1.21.0: Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk

検証済みサンプル — golang github.com/spf13/viper v1.21.0: Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it…

sha256:ea36c1c79c3a4263305e79d2403494634711a275d2fd41a2d8df834ac7e9101b

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 MIT-0

実行証拠

宣言された環境と署名済みの実行を分けてあります。このサンプルが何をどこで実行したかをそのまま確認できます。

証拠の基準
署名済みコントラクト合格
検証レシート
3
ビルドした署名鍵
3
宣言された環境 go 1.26 linux x64 go 1.26 go go

検証実行環境

環境 コントラクト ステージ 実行日
go 1.26 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-14
go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-18
go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f…
2026-09-08

ケース

HOW
ゴール
Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk
パッケージ
シンボル
  • viper.New
  • viper.SetDefault
  • viper.Set
  • viper.BindPFlag
  • viper.AutomaticEnv
  • viper.SetEnvPrefix
  • viper.SetEnvKeyReplacer
  • viper.BindEnv
  • viper.AllowEmptyEnv
  • viper.ReadConfig
  • viper.AddRemoteProvider
  • viper.RemoteConfig
  • viper.ReadRemoteConfig
  • viper.IsSet
  • viper.Get
  • viper.AllKeys
  • viper.Unmarshal
  • viper.ExperimentalBindStruct
環境
go 1.26
作成日
2026-08-14T13:24:24Z

コントラクト

  1. assert the documented ladder top down by writing the same key at every rung at once: explicit Set beats a changed flag beats env beats config file beats key/value store beats default, each rung checked with everything below it populated
  2. assert the key/value rung is reachable offline through viper.RemoteConfig, whose interface type is unexported but whose three methods are not, so the one rung nobody ever sees is observable without an etcd server
  3. assert a bound flag only sits at flag height once pflag records it as Changed, and that its default is consulted last of all, below SetDefault, so a flag default loses to a config file
  4. assert IsSet searches with that flag-default pass switched off, so it returns false for a key whose Get returns the flag's default
  5. assert AutomaticEnv computes the name as uppercase(prefix + underscore + key) by exporting the literal name CSX_LOG.LEVEL and watching a dotted key find it, which no shell could have exported
  6. assert that without SetEnvKeyReplacer the expected CSX_LOG_LEVEL is never consulted for the key log.level, and that installing the replacer reaches it
  7. assert the underscore between prefix and key is added by viper, so SetEnvPrefix("csx_") looks up CSX__LOG_LEVEL
  8. assert the replacer runs over the finished name rather than the key alone, so SetEnvPrefix("csx.app") with a dot replacer looks up CSX_APP_LOG_LEVEL
  9. assert an env var exported empty is treated as unset and falls through to the config file, until AllowEmptyEnv(true) makes the empty string a value that IsSet agrees is set
  10. assert BindEnv with one argument means prefix plus key, while a second argument is the env name verbatim with no prefix, no uppercasing and no replacer
  11. assert Get, GetString, GetInt, GetBool and GetDuration return zero values rather than errors on a missing key, so a typo is indistinguishable from a zero, and IsSet is the only thing that tells them apart
  12. assert, measured, that IsSet is true for a key that only has a default, while Set(key, nil) is invisible to the search and does not even shadow the default underneath it
  13. assert keys are case-insensitive and stored folded: Get agrees across spellings, AllKeys reports only the lower-case form, and a document's keys are folded on read while its values are left alone
  14. assert Unmarshal enumerates AllKeys rather than searching per key, so a value AutomaticEnv makes Get return is absent from both AllSettings and the struct, and that either a default for the key or viper.ExperimentalBindStruct brings it back

ファイル

  • csx.json
  • go.mod
  • go.sum
  • src/config.go
  • test/main.go

ソースアーティファクトをダウンロード (tar.gz)

ソース

csx.json
{"case":{"caseId":"case:sha256:49a00062ac22ba403d7fad6003e0e7adb4f024adef22bf71a527a801c0d9a459","constraints":{"runtime":"go"},"contract":["assert the documented ladder top down by writing the same key at every rung at once: explicit Set beats a changed flag beats env beats config file beats key/value store beats default, each rung checked with everything below it populated","assert the key/value rung is reachable offline through viper.RemoteConfig, whose interface type is unexported but whose three methods are not, so the one rung nobody ever sees is observable without an etcd server","assert a bound flag only sits at flag height once pflag records it as Changed, and that its default is consulted last of all, below SetDefault, so a flag default loses to a config file","assert IsSet searches with that flag-default pass switched off, so it returns false for a key whose Get returns the flag's default","assert AutomaticEnv computes the name as uppercase(prefix + underscore + key) by exporting the literal name CSX_LOG.LEVEL and watching a dotted key find it, which no shell could have exported","assert that without SetEnvKeyReplacer the expected CSX_LOG_LEVEL is never consulted for the key log.level, and that installing the replacer reaches it","assert the underscore between prefix and key is added by viper, so SetEnvPrefix(\"csx_\") looks up CSX__LOG_LEVEL","assert the replacer runs over the finished name rather than the key alone, so SetEnvPrefix(\"csx.app\") with a dot replacer looks up CSX_APP_LOG_LEVEL","assert an env var exported empty is treated as unset and falls through to the config file, until AllowEmptyEnv(true) makes the empty string a value that IsSet agrees is set","assert BindEnv with one argument means prefix plus key, while a second argument is the env name verbatim with no prefix, no uppercasing and no replacer","assert Get, GetString, GetInt, GetBool and GetDuration return zero values rather than errors on a missing key, so a typo is indistinguishable from a zero, and IsSet is the only thing that tells them apart","assert, measured, that IsSet is true for a key that only has a default, while Set(key, nil) is invisible to the search and does not even shadow the default underneath it","assert keys are case-insensitive and stored folded: Get agrees across spellings, AllKeys reports only the lower-case form, and a document's keys are folded on read while its values are left alone","assert Unmarshal enumerates AllKeys rather than searching per key, so a value AutomaticEnv makes Get return is absent from both AllSettings and the struct, and that either a default for the key or viper.ExperimentalBindStruct brings it back"],"goal":"Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk","kind":"HOW","packages":["pkg:golang/github.com/spf13/viper@1.21.0","pkg:golang/github.com/spf13/pflag@1.0.10"],"schemaVersion":1,"symbols":["viper.New","viper.SetDefault","viper.Set","viper.BindPFlag","viper.AutomaticEnv","viper.SetEnvPrefix","viper.SetEnvKeyReplacer","viper.BindEnv","viper.AllowEmptyEnv","viper.ReadConfig","viper.AddRemoteProvider","viper.RemoteConfig","viper.ReadRemoteConfig","viper.IsSet","viper.Get","viper.AllKeys","viper.Unmarshal","viper.ExperimentalBindStruct"]},"contractCommand":["go","run","./test"],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"go","runtime":"go","runtimeVersion":"1.26","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/github.com/spf13/viper@1.21.0","pkg:golang/github.com/spf13/pflag@1.0.10"],"schemaVersion":1,"symbols":["viper.New","viper.SetDefault","viper.Set","viper.BindPFlag","viper.AutomaticEnv","viper.SetEnvPrefix","viper.SetEnvKeyReplacer","viper.BindEnv","viper.AllowEmptyEnv","viper.ReadConfig","viper.AddRemoteProvider","viper.RemoteConfig","viper.ReadRemoteConfig","viper.IsSet","viper.Get","viper.AllKeys","viper.Unmarshal","viper.ExperimentalBindStruct"],"verifierAdapter":"golang@1"}
go.mod
module codesamplex.dev/sample/goviper

go 1.24

require (
	github.com/spf13/pflag v1.0.10
	github.com/spf13/viper v1.21.0
)

require (
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/sagikazarmark/locafero v0.11.0 // indirect
	github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
	github.com/spf13/afero v1.15.0 // indirect
	github.com/spf13/cast v1.10.0 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/text v0.28.0 // indirect
)
go.sum
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
src/config.go
// Package config wires spf13/viper the way a program does, and exposes the
// pieces needed to watch its precedence ladder decide a value.
//
// The documented order is: explicit Set, then flag, then environment, then
// config file, then key/value store, then default. What that sentence hides
// is that "flag" means a flag the user actually changed. viper checks
// flag.HasChanged() near the top of the search, and consults the flag's
// *default* only after everything else has failed — below SetDefault. So a
// flag default never overrides a config file, which is the opposite of what
// most people assume when they wire BindPFlag once and move on.
//
// Nothing here reads a path from disk. SetConfigType plus ReadConfig(io.Reader)
// is the whole file-loading path minus the file, which is also the cheapest
// way to test config handling without a fixture directory.
package config

import (
	"bytes"
	"io"
	"strings"

	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

// Key is the key every rung of the ladder writes. It is dotted on purpose: a
// dot is legal in a viper key and no shell can export a variable whose name
// contains one, and that mismatch is where the env lookup silently stops
// working.
const Key = "log.level"

// EnvPrefix is glued to the key as strings.ToUpper(prefix + "_" + key), so
// the prefix is written without its own trailing underscore.
const EnvPrefix = "csx"

// Level names one rung, highest priority first.
type Level int

const (
	LevelSet Level = iota
	LevelFlag
	LevelEnv
	LevelConfig
	LevelKV
	LevelDefault
)

func (l Level) String() string {
	return [...]string{"Set", "flag", "env", "config file", "key/value store", "default"}[l]
}

// New returns the viper an application ends up with: a format for readers
// that have no filename, a prefix so the process cannot be steered by a bare
// PATH-like name, a replacer so dotted keys have a legal env spelling, and
// AutomaticEnv so every Get consults the environment.
//
// Order does not matter among these four — each sets one field that is read
// at lookup time, not at call time — but all four are load-bearing: drop the
// replacer and "log.level" is looked up as CSX_LOG.LEVEL.
func New(opts ...viper.Option) *viper.Viper {
	v := viper.NewWithOptions(opts...)
	v.SetConfigType("yaml")
	v.SetEnvPrefix(EnvPrefix)
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.AutomaticEnv()
	return v
}

// ReadDocument fills the config-file rung from memory. ReadConfig needs
// SetConfigType because there is no extension to guess from.
func ReadDocument(v *viper.Viper, document string) error {
	return v.ReadConfig(bytes.NewBufferString(document))
}

// FlagSet is the flag rung. The flag is spelled log-level and the viper key
// is log.level; BindPFlag is the translation between them, which is why the
// two spellings never have to agree.
func FlagSet(defaultValue string) *pflag.FlagSet {
	fs := pflag.NewFlagSet("csxtool", pflag.ContinueOnError)
	fs.String("log-level", defaultValue, "logging level")
	return fs
}

// offlineKV stands in for etcd or consul. viper reaches its key/value rung
// through the package-level viper.RemoteConfig hook, whose interface type is
// unexported but whose three methods are not — so any type carrying them can
// be plugged in, and the rung nobody ever sees becomes observable without a
// server. Being package-level, this affects every *Viper in the process.
type offlineKV struct{ document string }

func (k offlineKV) Get(viper.RemoteProvider) (io.Reader, error) {
	return strings.NewReader(k.document), nil
}

func (k offlineKV) Watch(viper.RemoteProvider) (io.Reader, error) {
	return strings.NewReader(k.document), nil
}

func (k offlineKV) WatchChannel(viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) {
	return nil, nil
}

// ReadKeyValue fills the key/value rung. The payload is parsed with the same
// codec as the config file, so SetConfigType has to be in place first.
func ReadKeyValue(v *viper.Viper, document string) error {
	viper.RemoteConfig = offlineKV{document: document}
	if err := v.AddRemoteProvider("etcd3", "http://127.0.0.1:2379", "/csx"); err != nil {
		return err
	}
	return v.ReadRemoteConfig()
}

// Settings is the struct half of the API. Unmarshal builds its input from
// AllKeys, which is a different question than the one Get answers.
type Settings struct {
	Log struct {
		Level string `mapstructure:"level"`
	} `mapstructure:"log"`
}
test/main.go
package main

import (
	"fmt"
	"os"
	"slices"
	"strings"

	"github.com/spf13/viper"

	"codesamplex.dev/sample/goviper/src"
)

const document = `log:
  level: config
timeout: 30s
`

const kvDocument = `log:
  level: kv
`

// The env name viper computes for "log.level" under prefix "csx": uppercase
// the prefix, an underscore, then the key with the replacer applied.
const envName = "CSX_LOG_LEVEL"

func main() {
	precedence()
	flagDefaultIsLast()
	envNaming()
	emptyEnvIsNoEnv()
	bindEnvNaming()
	missingKeys()
	caseFolding()
	unmarshalSeesDifferentKeys()

	fmt.Println("contract ok")
}

// ladder builds a viper with every rung from top down to the default filled
// in, each writing the name of its own rung to the same key. Whatever Get
// returns is the name of the rung that won.
func ladder(top config.Level) *viper.Viper {
	v := config.New()
	v.SetDefault(config.Key, "default")
	if top <= config.LevelKV {
		must(config.ReadKeyValue(v, kvDocument))
	}
	if top <= config.LevelConfig {
		must(config.ReadDocument(v, document))
	}
	if top <= config.LevelEnv {
		must(os.Setenv(envName, "env"))
	} else {
		must(os.Unsetenv(envName))
	}
	if top <= config.LevelFlag {
		fs := config.FlagSet("flag default")
		must(fs.Parse([]string{"--log-level=flag"}))
		must(v.BindPFlag(config.Key, fs.Lookup("log-level")))
	}
	if top <= config.LevelSet {
		v.Set(config.Key, "set")
	}
	return v
}

func precedence() {
	for _, tc := range []struct {
		top  config.Level
		want string
	}{
		{config.LevelSet, "set"},
		{config.LevelFlag, "flag"},
		{config.LevelEnv, "env"},
		{config.LevelConfig, "config"},
		{config.LevelKV, "kv"},
		{config.LevelDefault, "default"},
	} {
		got := ladder(tc.top).GetString(config.Key)
		check(got == tc.want, "%v should outrank everything below it: got %q, want %q",
			tc.top, got, tc.want)
	}
	must(os.Unsetenv(envName))
}

// The rung that is not where people put it. A bound flag only enters the
// search at flag height once pflag records it as Changed; until then its
// default sits at the very bottom, below SetDefault.
func flagDefaultIsLast() {
	v := config.New()
	fs := config.FlagSet("flag default")
	must(v.BindPFlag(config.Key, fs.Lookup("log-level")))

	// Nothing else is set, so the flag default is all there is.
	check(v.GetString(config.Key) == "flag default", "unparsed flag: %q", v.GetString(config.Key))
	// IsSet searches with the flag-default pass switched off, so it disagrees
	// with Get about whether this key has a value. A guard written as
	// `if !v.IsSet(k) { return errors.New("required") }` rejects a key whose
	// Get would have returned the flag's default perfectly well.
	check(!v.IsSet(config.Key), "IsSet must ignore a flag default that was never changed")

	// A viper default outranks it, which is the wrong way round if you think
	// of the flag as living at flag height.
	v.SetDefault(config.Key, "default")
	check(v.GetString(config.Key) == "default", "SetDefault should beat an unchanged flag: %q",
		v.GetString(config.Key))
	check(v.IsSet(config.Key), "a default is enough for IsSet")

	// Parse the flag and it jumps the whole ladder.
	must(fs.Parse([]string{"--log-level=flag"}))
	check(v.GetString(config.Key) == "flag", "a changed flag should win: %q", v.GetString(config.Key))

	// The consequence, spelled out rather than inferred from the two steps
	// above: bind a flag, never parse it, and the config file buries its
	// default, even though the flag sits above the file in the documented order.
	buried := config.New()
	unparsed := config.FlagSet("flag default")
	must(buried.BindPFlag(config.Key, unparsed.Lookup("log-level")))
	must(config.ReadDocument(buried, document))
	check(buried.GetString(config.Key) == "config",
		"a config file should bury an unparsed flag's default: %q", buried.GetString(config.Key))
}

// AutomaticEnv does not look for CSX_LOG_LEVEL because someone decided it
// should. It builds the name mechanically, and the replacer is the only part
// of that name a dotted key can supply.
func envNaming() {
	bare := viper.New()
	bare.SetConfigType("yaml")
	bare.SetEnvPrefix(config.EnvPrefix)
	bare.AutomaticEnv()
	must(config.ReadDocument(bare, document))

	must(os.Setenv(envName, "env"))
	check(bare.GetString(config.Key) == "config",
		"without SetEnvKeyReplacer, %s is never consulted: %q", envName, bare.GetString(config.Key))

	// The name it does consult keeps the dot, which no shell can export. Go's
	// os.Setenv only refuses '=' and NUL, so the unreachable name is reachable
	// from here, and that is the proof the lookup happens at all.
	must(os.Setenv("CSX_LOG.LEVEL", "dotted"))
	check(bare.GetString(config.Key) == "dotted",
		"the computed name is uppercase(prefix + _ + key), dot intact: %q", bare.GetString(config.Key))
	must(os.Unsetenv("CSX_LOG.LEVEL"))

	// With the replacer installed, the spelling people expect is the spelling
	// that works.
	check(config.New().GetString(config.Key) == "env", "replacer should reach %s", envName)

	// The replacer runs over the finished name, prefix included: viper glues
	// the prefix on and uppercases, then replaces. A prefix carrying a
	// character the replacer rewrites gets rewritten too, which is easy to miss
	// when the replacer was added for the key's sake. Were it applied to the
	// key alone, the lookup here would be CSX.APP_LOG_LEVEL and find nothing.
	dottedPrefix := viper.New()
	dottedPrefix.SetEnvPrefix("csx.app")
	dottedPrefix.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	dottedPrefix.AutomaticEnv()
	must(os.Setenv("CSX_APP_LOG_LEVEL", "prefix replaced"))
	check(dottedPrefix.GetString(config.Key) == "prefix replaced",
		"the replacer rewrites the prefix too: %q", dottedPrefix.GetString(config.Key))
	must(os.Unsetenv("CSX_APP_LOG_LEVEL"))

	// The underscore between prefix and key is added by viper, so a prefix
	// written with one of its own doubles it.
	doubled := viper.New()
	doubled.SetEnvPrefix(config.EnvPrefix + "_")
	doubled.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	doubled.AutomaticEnv()
	must(os.Setenv("CSX__LOG_LEVEL", "doubled"))
	check(doubled.GetString(config.Key) == "doubled",
		"SetEnvPrefix(%q) should look up CSX__LOG_LEVEL: %q", config.EnvPrefix+"_", doubled.GetString(config.Key))
	must(os.Unsetenv("CSX__LOG_LEVEL"))
	must(os.Unsetenv(envName))
}

// An exported-but-empty variable is a real state for a shell, and viper
// throws it away unless told not to.
func emptyEnvIsNoEnv() {
	v := config.New()
	must(config.ReadDocument(v, document))
	must(os.Setenv(envName, ""))
	check(v.GetString(config.Key) == "config",
		"an empty env var falls through by default: %q", v.GetString(config.Key))

	allow := config.New()
	allow.AllowEmptyEnv(true)
	must(config.ReadDocument(allow, document))
	check(allow.GetString(config.Key) == "",
		"AllowEmptyEnv(true) makes the empty string a value: %q", allow.GetString(config.Key))
	check(allow.IsSet(config.Key), "and IsSet agrees it is set, even though the value is empty")

	// With nothing underneath to fall through to, the same difference decides
	// whether the key exists at all.
	must(os.Setenv("CSX_TRACE_ID", ""))
	check(!v.IsSet("trace.id"), "an env var exported empty leaves its key unset")
	check(allow.IsSet("trace.id"), "and set once empty values are allowed")
	must(os.Unsetenv("CSX_TRACE_ID"))
	must(os.Unsetenv(envName))
}

// BindEnv is the explicit half of the same machinery, and its two forms do
// not agree about the prefix.
func bindEnvNaming() {
	must(os.Setenv("CSX_PORT", "8080"))
	must(os.Setenv("SERVICE_PORT", "9090"))

	prefixed := viper.New()
	prefixed.SetEnvPrefix(config.EnvPrefix)
	must(prefixed.BindEnv("port"))
	// Values arrive from the environment as strings; the Get* family casts.
	check(prefixed.GetInt("port") == 8080, "one argument means prefix + key: %v", prefixed.Get("port"))

	explicit := viper.New()
	explicit.SetEnvPrefix(config.EnvPrefix)
	must(explicit.BindEnv("port", "SERVICE_PORT"))
	check(explicit.GetInt("port") == 9090,
		"a second argument is the env name verbatim: no prefix, no uppercasing, no replacer: %v",
		explicit.Get("port"))

	must(os.Unsetenv("CSX_PORT"))
	must(os.Unsetenv("SERVICE_PORT"))
}

// Nothing in the Get family returns an error, so a typo reads as a zero.
func missingKeys() {
	v := config.New()
	check(v.Get("nope") == nil, "Get on a missing key returns a nil any: %v", v.Get("nope"))
	check(v.GetString("nope") == "", "GetString: %q", v.GetString("nope"))
	check(v.GetInt("nope") == 0, "GetInt: %d", v.GetInt("nope"))
	check(!v.GetBool("nope"), "GetBool")
	check(v.GetDuration("nope") == 0, "GetDuration: %v", v.GetDuration("nope"))
	check(!v.IsSet("nope"), "IsSet is the only one that can tell you it was missing")

	// So a key really set to its zero value is indistinguishable from a typo
	// by Get alone, and IsSet is the whole answer.
	v.Set("retries", 0)
	check(v.GetInt("retries") == 0 && v.IsSet("retries"), "an explicit zero is set")

	// Measured, and worth knowing before writing "IsSet means the user chose
	// it": a key that only has a default is set. IsSet skips just one rung of
	// the search, the flag default, and defaults are not that rung.
	d := config.New()
	d.SetDefault("timeout", "30s")
	check(d.IsSet("timeout"), "IsSet is true for a key that only has a default")

	// The exception at the other end: Set(key, nil) stores nothing findable,
	// because nil is also how the search says "not here". It does not even
	// shadow the default underneath it.
	n := config.New()
	n.SetDefault("retries", 3)
	n.Set("retries", nil)
	check(n.GetInt("retries") == 3, "Set(key, nil) is invisible to the search: %v", n.Get("retries"))
}

func caseFolding() {
	v := config.New()
	v.Set("Feature.Flag", "ON")
	check(v.Get("feature.flag") == "ON", "lower: %v", v.Get("feature.flag"))
	check(v.Get("FEATURE.FLAG") == "ON", "upper: %v", v.Get("FEATURE.FLAG"))
	check(v.Get("Feature.Flag") == "ON", "as written: %v", v.Get("Feature.Flag"))
	// One key, stored once, under the folded spelling.
	check(slices.Contains(v.AllKeys(), "feature.flag"), "AllKeys: %v", v.AllKeys())
	check(!slices.Contains(v.AllKeys(), "Feature.Flag"), "AllKeys keeps no original spelling: %v", v.AllKeys())

	// Keys read from a document are folded on the way in. Values are not
	// touched, so only half of a case-sensitive comparison changes under you.
	mixed := config.New()
	must(config.ReadDocument(mixed, "Log:\n  Level: Config\n"))
	check(mixed.GetString(config.Key) == "Config", "keys fold, values do not: %q", mixed.GetString(config.Key))
}

// Get and Unmarshal ask different questions. Get searches the rungs for one
// key; Unmarshal enumerates AllKeys and reads each one. AutomaticEnv
// contributes no keys to that enumeration — it cannot, since it would have to
// guess names — so a value Get returns can be missing from the struct.
func unmarshalSeesDifferentKeys() {
	must(os.Setenv(envName, "env"))

	v := config.New()
	check(v.GetString(config.Key) == "env", "Get sees it: %q", v.GetString(config.Key))
	check(len(v.AllSettings()) == 0, "AllSettings does not: %v", v.AllSettings())

	var s config.Settings
	must(v.Unmarshal(&s))
	check(s.Log.Level == "", "Unmarshal walks AllKeys, so the field stays empty: %q", s.Log.Level)

	// Fix one: make the key known by any rung at all. The default never wins
	// the lookup — it only puts the key in AllKeys so Unmarshal asks for it.
	known := config.New()
	known.SetDefault(config.Key, "default")
	var s2 config.Settings
	must(known.Unmarshal(&s2))
	check(s2.Log.Level == "env", "a default makes the key enumerable: %q", s2.Log.Level)

	// Fix two: let viper take the key list from the destination struct.
	bound := config.New(viper.ExperimentalBindStruct())
	var s3 config.Settings
	must(bound.Unmarshal(&s3))
	check(s3.Log.Level == "env", "ExperimentalBindStruct adds the struct's keys: %q", s3.Log.Level)

	must(os.Unsetenv(envName))
}

func must(err error) {
	check(err == nil, "unexpected error: %v", err)
}

func check(ok bool, format string, args ...any) {
	if !ok {
		fmt.Fprintf(os.Stderr, format+"\n", args...)
		os.Exit(1)
	}
}

オリジンシーダー

anonymous