Source-verified agent skills for Go: Cobra command-line interfaces, Viper configuration, and Wails desktop apps, each backed by tested examples.
99
99%
Does it follow best practices?
Impact
100%
1.02xAverage score across 18 eval scenarios
Passed
No findings from the security scan
Load settings with Viper so they stay predictable and testable: a fresh instance per load, every key registered explicitly, one file chosen on purpose, a typed and validated snapshot handed to the rest of the program, and no config loading on paths that do not need it. The pitfalls below are the precedence, environment, file, and decoding behaviors that cause real bugs.
go.mod. Use only APIs available in its Viper version;
do not upgrade a dependency just to match this skill. Write Go for the
version in its go directive (if a modern-Go guidelines skill such as
use-modern-go is installed, follow it for that version).One function builds a new instance, registers every layer, reads at most one file, decodes, and validates:
// examples/viper/config/config.go (trimmed: types, AddFlags, and resolve omitted)
func Load(src Source) (Config, error) {
v := viper.New()
for key, value := range defaults {
v.SetDefault(key, value)
}
v.SetEnvPrefix(EnvPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
for key := range defaults {
if err := v.BindEnv(key); err != nil {
return Config{}, err
}
}
for _, key := range envOnly {
if err := v.BindEnv(key); err != nil {
return Config{}, err
}
}
if src.Flags != nil {
for name, key := range flagKeys {
if f := src.Flags.Lookup(name); f != nil {
if err := v.BindPFlag(key, f); err != nil {
return Config{}, err
}
}
}
}
path, err := resolve(src)
if err != nil {
return Config{}, err
}
if path != "" {
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Config{}, fmt.Errorf("read %s: %w", path, err)
}
}
var cfg Config
if err := v.UnmarshalExact(&cfg); err != nil {
return Config{}, fmt.Errorf("decode config: %w", err)
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}viper.New() per load or command tree, not the package-level
singleton. Separate instances share nothing, so tests do not leak state.Config to the core. Keep *viper.Viper out of business
logic.UnmarshalExact, an
operational flag such as --json bound through BindPFlags becomes an
unknown key.The full loader, its file resolution, and its test matrix are in references/loading.md.
Highest first, as Get and Unmarshal resolve a key: Set, a flag the user
changed, environment, config file, SetDefault, then an unchanged flag's own
default.
SetDefault. Copying parsed flag values into Set makes an
omitted flag's default beat env and the file.--tls=false is a changed flag and wins over a true default
or file value. Never decide "unset" with value == zero.StringVar, IntVar, and so on. Read effective values from the decoded
struct after parsing.BindPFlag returns an error for a nil flag, which is what Lookup returns
for a misspelled name. Check it.AutomaticEnv answers Get for any key but adds nothing to AllKeys, so
Unmarshal misses an env-only key. Give every decoded key a SetDefault
or a BindEnv.BindEnv("server.read-timeout") derives PREFIX_SERVER.READ-TIMEOUT in
upper case, and SetEnvKeyReplacer then turns it into
APP_SERVER_READ_TIMEOUT. BindEnv(key, "DATABASE_PASSWORD") uses that
name exactly, without the prefix.AllowEmptyEnv(true).labels: {Team: Core} decodes as team). Keep case-sensitive data in a
list of structs, not in map keys.SetConfigName and AddConfigPath tries extensions in the
order of viper.SupportedExts, so app.json wins over app.yaml in the
same directory. SetConfigType("yaml") does not filter that search; it
forces the YAML parser on whatever was found, including app.toml. For a
single-format app, resolve the exact file yourself and call SetConfigFile.viper.ConfigFileNotFoundError; ignore only that.
A missing SetConfigFile path returns a plain fs.ErrNotExist error, so a
user-supplied --config that does not exist still fails, as it should.
Match it as a value type, with errors.AsType or with errors.As and a
viper.ConfigFileNotFoundError variable; a pointer target never matches.SetConfigType, or it fails with
UnsupportedConfigError. SetConfigType also overrides an explicit file's
extension..ini, .hcl, and
.properties fail with "decoder not found for this format"; those codecs
live in github.com/go-viper/encoding and need a codec registry.Unmarshal ignores unknown keys; UnmarshalExact rejects them, which
catches typos in a closed schema.GetInt returns 0 for eighty; Unmarshal reports the error. Decode, do
not trust getters, to detect bad input.Unmarshal splits a slice on commas only
(a,b c gives a and b c) while GetStringSlice splits on whitespace
(a,b and c).viper.DecodeHook replaces the default hooks (duration and comma-slice
parsing). Compose them back with
mapstructure.ComposeDecodeHookFunc from github.com/go-viper/mapstructure/v2.Unmarshal, and report them together.Cobra runs a root PersistentPreRunE for completion, every hidden
__complete request, and help <command>. Loading config there breaks tab
completion on a machine whose config is missing or broken. Load in the
RunE of each command that needs settings, after Cobra has parsed flags:
// examples/viper/cli/cli.go (trimmed: root flags and completion omitted)
RunE: func(cmd *cobra.Command, _ []string) error {
cfg, err := config.Load(config.Source{File: file, Dirs: dirs, Flags: cmd.Flags()})
if err != nil {
return err
}
return serve(cmd.Context(), cfg)
},MarkFlagRequired checks only that a flag was typed. When env or a file can
supply the setting, validate the decoded value instead. For command trees,
hooks, and output streams, use the
cobra skill
if it is installed.
WatchConfig semantics and a safe reload pattern, writing config files,
Sub, and answering "where did this value come from".Before finishing, test the loader with a fresh instance, t.TempDir() files,
and t.Setenv for every variable it reads (cleared first, since ambient
values on a CI runner change results; do not use t.Parallel with
t.Setenv). Cover:
--config path that is absent;Run go vet and go test for the packages you changed. When a check fails,
find the matching entry in Precedence and environment
or Files and decoding, fix the loader, and rerun every
check.
Verified against viper v1.21.0 with pflag v1.0.10 and cobra v1.10.2; each
stated behavior has a test in the
examples module.
When the project's version differs, trust go doc github.com/spf13/viper and
its module source over this skill, and recheck the error types after any
upgrade.