PluginBench
Skill
Pass
Audit score 90

golang-spf13-viper

samber/cc-skills-golang

Layered configuration resolution for Go: flags > env > files > defaults with hot reload support.

What is golang-spf13-viper?

spf13/viper is a configuration library that resolves values from multiple sources in fixed precedence order: explicit Set, flags, environment variables, config files, remote KV, and defaults. Use it when building Go applications that need to merge configuration from multiple sources—CLIs with flags, services reading config files, or systems that support environment variable overrides.

  • Resolve configuration keys across four layers (flags, env vars, config files, defaults) with fixed precedence
  • Bind cobra flags and environment variables to viper keys with automatic prefix and key replacement
  • Load and parse config files (JSON, TOML, YAML, HCL, INI) with graceful handling of missing files
  • Unmarshal configuration into Go structs using mapstructure tags
  • Watch config files for changes and trigger callbacks on reload
  • Create isolated viper instances for test isolation without global state leakage

How to install golang-spf13-viper

npx skills add https://github.com/samber/cc-skills-golang --skill golang-spf13-viper
Prerequisites
  • Go installed and project initialized
  • github.com/spf13/viper imported: `go get github.com/spf13/viper@latest`
Claude Code
Cursor
Windsurf
Cline

How to use golang-spf13-viper

  1. 1.Set config file name and search paths with SetConfigName and AddConfigPath
  2. 2.Call ReadInConfig and handle ConfigFileNotFoundError gracefully (missing files are often valid)
  3. 3.Wire env binding: SetEnvPrefix, SetEnvKeyReplacer, and AutomaticEnv together at startup
  4. 4.Bind cobra flags with BindPFlag or BindPFlags in init() or PersistentPreRunE, never in RunE
  5. 5.Define a config struct with mapstructure tags and call Unmarshal to populate it
  6. 6.For hot reload, call WatchConfig and OnConfigChange; test with `echo >> config.yaml` not editor saves
  7. 7.In tests, use viper.New() per test case instead of the global viper to avoid state leakage

Use cases

Good for
  • Building CLI tools that accept both flags and config files with environment variable overrides
  • Configuring microservices that read from config files but allow env var overrides in containers
  • Implementing hot-reload for long-running daemons that watch config files for changes
  • Merging configuration from multiple sources (flags, env, files) with predictable precedence
  • Testing configuration logic in isolation without affecting other test cases
Who it's for
  • Go backend engineers building CLI tools or services
  • DevOps engineers configuring containerized applications with env vars
  • Teams adopting spf13/cobra for command structure and needing configuration management

golang-spf13-viper FAQ

What is the precedence order for configuration sources?

Viper walks sources in fixed order: explicit Set() > flags > env vars > config files > remote KV > defaults. The first source that has a value wins; you cannot reorder this pipeline.

Why doesn't my environment variable override work for nested keys like database.host?

You must wire three settings together: SetEnvPrefix (e.g., 'MYAPP'), SetEnvKeyReplacer to convert dots to underscores, and AutomaticEnv(). Missing any one causes nested keys to fail silently. Without SetEnvKeyReplacer, viper looks for MYAPP_DATABASE.HOST instead of MYAPP_DATABASE_HOST.

Should I handle ConfigFileNotFoundError?

Yes. Always check for ConfigFileNotFoundError and handle it gracefully—missing config files are usually valid when the application runs with only flags or env vars. Only propagate real errors (permission denied, parse errors).

When should I bind cobra flags to viper?

Bind flags in init() or PersistentPreRunE, never in RunE. Cobra parses flags before RunE runs, so bindings set in RunE are missed and flags won't override config file values.

Why should I use viper.New() in tests instead of the global viper?

The global viper accumulates state across test cases (Set values, bound flags, loaded files). Each test should use viper.New() to create an isolated instance with no cross-test pollution.

Full instructions (SKILL.md)

Source of truth, from samber/cc-skills-golang.


name: golang-spf13-viper description: "Golang configuration library using spf13/viper — layered precedence (flag > env > file > KV > default), BindPFlag/BindPFlags, SetEnvPrefix + SetEnvKeyReplacer + AutomaticEnv, ReadInConfig + ConfigFileNotFoundError, Unmarshal + mapstructure struct tags, Sub for sub-trees, WatchConfig + OnConfigChange for hot reload, viper.New() for test isolation, and remote KV integration. Apply when using or adopting spf13/viper, or when the codebase imports github.com/spf13/viper. For CLI command structure alongside viper, see the samber/cc-skills-golang@golang-spf13-cobra skill. For general CLI architecture, see samber/cc-skills-golang@golang-cli." user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber version: "1.0.2" openclaw: emoji: "🔧" homepage: https://github.com/samber/cc-skills-golang requires: bins: - go install: [] skill-library-version: "1.21.0" allowed-tools: Read Edit Write Glob Grep Bash(go:) Bash(golangci-lint:) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs

Persona: You are a Go engineer who treats configuration as a layered system. Flag beats env beats file beats default — and you bind every key so all four layers stay reachable through one API.

Using spf13/viper for layered configuration in Go

Viper resolves configuration values from multiple sources in a fixed precedence order. It has no user-facing surface — it doesn't define commands or flags. Its job is to answer "what is the value of key X right now?" by walking its source layers from highest to lowest priority.

Official Resources:

This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform. For Go package docs, versions, symbols, and known vulnerabilities, → See samber/cc-skills-golang@golang-pkg-go-dev skill.

go get github.com/spf13/viper@latest

Viper vs. cobra

Cobra owns the command tree — subcommands, flags, arg validation, completions. Viper owns configuration resolution — it answers "what is the value of key X?" by walking its source layers. Viper has no user-facing surface; it is purely a key-value resolver. Use cobra alone for flag-only CLIs; viper alone for config-file daemons; both when you need both, binding flags at PersistentPreRunE via BindPFlag.

→ See samber/cc-skills-golang@golang-spf13-cobra for the cobra side of this integration.

The precedence pipeline

Viper resolves a key by walking sources in this order (first set value wins):

1. explicit Set()      — viper.Set("key", val)    highest priority
2. flag                — bound pflag.Flag
3. env var             — BindEnv / AutomaticEnv
4. config file         — ReadInConfig / MergeInConfig
5. KV remote           — etcd / Consul
6. default             — viper.SetDefault("key", val)   lowest priority

This pipeline is fixed and cannot be reordered. Understanding it prevents most viper bugs: a key that "should" come from a config file may be shadowed by an env var or a flag with a default value.

Sources and config files

viper.SetConfigName("config")
viper.AddConfigPath("$HOME/.myapp")
if err := viper.ReadInConfig(); err != nil {
    var notFound *viper.ConfigFileNotFoundError
    if !errors.As(err, &notFound) {
        return fmt.Errorf("reading config: %w", err) // propagate real errors only
    }
}

ConfigFileNotFoundError must be handled gracefully — config files are usually optional. An unhandled error from a missing file crashes programs that are perfectly valid when run with only flags or env vars.

For supported formats (JSON, TOML, YAML, HCL, INI, properties), MergeInConfig, and remote KV, see sources-and-formats.md.

Env binding and key replacers

This is the highest-bug-density area in viper. All three settings must be wired together — missing any one breaks nested key resolution:

// ✓ Good — all three wired together at startup
viper.SetEnvPrefix("MYAPP")                             // prevent collisions: PORT → MYAPP_PORT
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))  // database.host → MYAPP_DATABASE_HOST
viper.AutomaticEnv()

// ✗ Bad — without SetEnvKeyReplacer, viper looks for MYAPP_DATABASE.HOST (dot preserved)

For BindEnv, AllowEmptyEnv, and env-vs-default interaction, see binding-and-env.md.

Flag binding (the cobra seam)

Bind cobra flags to viper in init() or PersistentPreRunE — never in RunE (config loading in PersistentPreRunE already ran before RunE, so bindings set in RunE are missed):

func init() {
    rootCmd.PersistentFlags().Int("port", 8080, "listen port")
    viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
    // viper.BindPFlags(cmd.Flags()) — bind an entire FlagSet at once
}

For AllowEmptyEnv and flag/env interaction details, see binding-and-env.md.

Unmarshaling into structs

viper.Unmarshal maps the resolved configuration into a struct using mapstructure:

type Config struct {
    Port     int `mapstructure:"port"`
    Database struct {
        MaxConn int `mapstructure:"max_conn"` // explicit tag: mapstructure won't convert underscore→camelCase
    } `mapstructure:"database"`
}
var cfg Config
viper.Unmarshal(&cfg)

Always use mapstructure tags — implicit mapping is fragile for nested structs and underscore-named fields. Prefer UnmarshalKey("database", &dbCfg) over Sub("database").Unmarshal — it avoids the nil-check Sub requires when the key is missing.

For time.Duration / net.IP / slice decoders and custom DecodeHook registration, see unmarshal.md.

Sub-trees

viper.Sub("database") returns a new *viper.Viper scoped to the prefix, or nil if the key does not exist — always nil-check before calling methods on the result. Prefer UnmarshalKey("database", &dbCfg) which avoids the nil risk entirely.

Hot reload

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) { /* re-apply changed values */ })

WatchConfig uses fsnotify and watches inodes. Editors that write atomically via rename (vim, neovim) replace the inode — the callback may not fire. Test hot-reload with echo >> config.yaml, not editor saves. For race-safe reload patterns, see watch-and-reload.md.

Test isolation

Never use the global viper in tests — state leaks across test cases. Use viper.New() per test so each instance is isolated:

v := viper.New()
v.SetConfigFile("testdata/config.yaml")
require.NoError(t, v.ReadInConfig())

For t.Setenv interactions and Reset() limitations, see testing-and-isolation.md.

Best Practices

  1. Set prefix + key replacer + AutomaticEnv together — missing any one causes nested env keys to silently not resolve (database.hostDATABASE.HOST instead of DATABASE_HOST).
  2. Handle ConfigFileNotFoundError gracefully — a missing config file should not crash a service that runs with only flags and env vars.
  3. Always use mapstructure tags on config structs — implicit mapping silently misses nested and underscore-named fields.
  4. Use viper.New() in tests, never the global — the global accumulates state across test runs; per-test instances are isolated.
  5. Bind flags before Execute() — binding in RunE is too late; cobra parses flags before RunE runs.

Common Mistakes

MistakeWhy it failsFix
AutomaticEnv without SetEnvKeyReplacerdatabase.host looks for MYAPP_DATABASE.HOST (dot preserved) — never matchesAdd SetEnvKeyReplacer(strings.NewReplacer(".", "_")) before AutomaticEnv
No mapstructure tags on struct fieldsSilently misses nested and underscore-named fieldsAdd mapstructure:"key_name" to every field
Using global viper in testsState from one test contaminates the next, causing flaky orderingCreate viper.New() per test
Missing ConfigFileNotFoundError checkMissing config file crashes a service that should run on flags/env aloneerrors.As(err, &notFound) — only propagate non-not-found errors

Further Reading

  • sources-and-formats.md — supported file formats, multi-path search, MergeInConfig, remote KV (etcd/Consul)
  • binding-and-env.md — BindEnv, AutomaticEnv, SetEnvPrefix, SetEnvKeyReplacer, AllowEmptyEnv, timing rules
  • unmarshal.md — Unmarshal, UnmarshalKey, mapstructure tags, custom DecodeHooks (Duration, IP, slice)
  • watch-and-reload.md — WatchConfig, OnConfigChange, fsnotify caveats, atomic-rename trap, race-safe patterns
  • testing-and-isolation.md — viper.New() per test, t.Setenv interactions, Reset() limitations, snapshot/restore

Cross-References

  • → See samber/cc-skills-golang@golang-cli skill for general CLI architecture — project layout, exit codes, signal handling, cobra+viper integration
  • → See samber/cc-skills-golang@golang-spf13-cobra skill for the cobra side of this integration (flag definition and binding)
  • → See samber/cc-skills-golang@golang-testing skill for general Go testing patterns

If you encounter a bug or unexpected behavior in spf13/viper, open an issue at https://github.com/spf13/viper/issues.