3.8 KiB
Viper Env Binding and Flag Binding
The binding interaction model
Three settings control how viper maps environment variables to keys. They must be set together:
viper.SetEnvPrefix("MYAPP") // adds MYAPP_ prefix
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // database.host → MYAPP_DATABASE_HOST
viper.AutomaticEnv() // activates auto-binding
Call these before any ReadInConfig or viper.Get* call — typically in a root command's PersistentPreRunE or in init().
AutomaticEnv vs BindEnv
| Method | Behavior |
|---|---|
AutomaticEnv() |
Every key is automatically mapped to its env equivalent (with prefix and replacer applied) |
BindEnv(key, envVars...) |
Only the specified key is bound, to the specified env var name(s) |
Use AutomaticEnv for the common case. Use BindEnv when you need to bind to an env var with a name that doesn't follow your prefix/replacer convention (e.g., third-party env vars like GOOGLE_APPLICATION_CREDENTIALS).
// Bind a specific non-prefixed env var
viper.BindEnv("google.credentials", "GOOGLE_APPLICATION_CREDENTIALS")
SetEnvKeyReplacer in depth
Viper keys use . as separator for nested values. Env vars cannot contain dots. The replacer maps between them.
// Config file:
// database:
// host: localhost
// max_conn: 25
// Without replacer:
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.GetString("database.host") // looks for MYAPP_DATABASE.HOST — no match
// With replacer:
viper.SetEnvPrefix("MYAPP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.GetString("database.host") // looks for MYAPP_DATABASE_HOST — matches
The replacer operates on the viper key before prepending the prefix, so the lookup chain is: database.host → replace . with _ → database_host → prepend prefix → MYAPP_DATABASE_HOST.
AllowEmptyEnv
By default, viper ignores env vars set to the empty string — the empty string is treated as "not set" and viper continues down the precedence stack. Override this behavior:
viper.AllowEmptyEnv(true)
// now MYAPP_PORT="" → viper.GetInt("port") == 0, not the default
Flag binding
Bind a pflag after defining it:
func init() {
rootCmd.PersistentFlags().Int("port", 8080, "listen port")
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}
Bind an entire flag set:
viper.BindPFlags(rootCmd.PersistentFlags())
Timing rule: Bind flags in init() or in PersistentPreRunE. The binding call must happen before Execute() parses flags — specifically, before any viper.Get* call on a flag-backed key. Binding after Execute() causes the flag's Changed state to be unknown, so viper may not promote the flag value to the correct precedence layer.
How pflag binding interacts with precedence
Viper checks flag.Changed (whether the user explicitly passed the flag). This is how it distinguishes between "flag default" (low priority) and "flag explicitly set" (high priority):
flag.Changed == false(flag has its default): viper treats the flag as not present and falls through to env/file/default.flag.Changed == true(flag was provided on the command line): viper treats the flag value as the highest-priority source.
This means viper.GetInt("port") correctly returns the flag value when --port 9090 is passed, and falls back to env MYAPP_PORT or config file port: 8080 otherwise.
Debugging binding
Print all resolved values to verify your binding is correct:
fmt.Println(viper.AllSettings())
// map[database:map[host:localhost max_conn:25] port:8080]
Check env var resolution:
os.Setenv("MYAPP_PORT", "9090")
viper.AutomaticEnv()
fmt.Println(viper.GetInt("port")) // 9090