Building command-line tools in Go often leads to a common challenge: managing configuration from multiple sources. I faced this repeatedly while developing utilities for my team. We needed a way to handle flags, environment variables, and config files without complexity. That’s when I explored combining Cobra and Viper—a pairing that transformed how we handle configurations.
Cobra excels at structuring CLI commands. It handles flags, subcommands, and help documentation with elegance. Viper specializes in configuration management. It merges data from files, environment variables, and remote sources into a single interface. Together, they create a layered approach where command-line flags override environment variables, which supersede file-based settings. This hierarchy matches real-world deployment needs perfectly.
Let’s implement a basic integration. First, define a command with Cobra:
rootCmd := &cobra.Command{
Use: "myapp",
Run: func(cmd *cobra.Command, args []string) {
// Command logic here
},
}
Next, bind a flag to both Cobra and Viper:
rootCmd.PersistentFlags().String("config-path", "", "Config file path")
viper.BindPFlag("config_path", rootCmd.PersistentFlags().Lookup("config-path"))
Notice the underscore in config_path
? Viper automatically translates CONFIG_PATH
in environment variables to this key. Now, initialize Viper to read configurations:
viper.SetConfigName("config")
viper.AddConfigPath("/etc/myapp/")
viper.AddConfigPath("$HOME/.myapp")
viper.ReadInConfig() // Loads first valid config file
What happens if the same setting exists in a YAML file and as a flag? Viper resolves conflicts using this priority:
- Command-line flags (highest)
- Environment variables
- Configuration files
- Default values (lowest)
For sensitive data like API keys, use Viper’s encryption hooks. Here’s a snippet for AES-256 decryption:
viper.SetConfigType("env")
viper.ReadConfig(bytes.NewReader(encryptedConfig))
viper.Decrypt(decryptionKey) // Custom decryption logic
A practical use case: imagine a CLI tool connecting to a database. During development, settings live in config.yaml
. In Docker, environment variables inject credentials. For debugging, engineers override timeouts via flags. One code path handles all scenarios:
db.Connect(viper.GetString("db.host"), viper.GetInt("db.timeout"))
Hot-reloading configurations is remarkably useful. Viper can watch files for changes:
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Println("Config updated:", e.Name)
// Re-initialize components
})
Why not handle this manually? Without Viper, you’d write hundreds of lines for format parsing, precedence rules, and error handling. This integration reduces boilerplate while adding features like JSON, YAML, and etcd support.
For distributed tools, pair Viper with remote systems. This fetches configurations from Consul:
viper.AddRemoteProvider("consul", "localhost:8500", "myapp/config")
viper.ReadRemoteConfig()
Common pitfall: forgetting to bind flags before calling viper.BindPFlag
. The sequence matters. Also, always check viper.ConfigFileNotFoundError
—it’s safe to ignore when using fallback sources.
In production, this combo shines. Our deployment tools now support:
- Zero-config defaults for local testing
- Kubernetes-ready environment variables
- Last-minute flag overrides during incidents
- Automatic refresh for rate-limit rules
What would your tool look like with unified configuration? Could it simplify your deployment workflows?
Try this pattern. Start small:
- Scaffold commands with Cobra
- Bind critical flags to Viper
- Add
viper.AutomaticEnv()
for environment variables - Load a config file as optional
The reduction in support tickets surprised us. Engineers no longer ask, “Why isn’t my setting applied?” because the hierarchy behaves predictably.
If you’ve battled configuration chaos, this approach might help. Share your results in the comments—I’d love to hear how it works for you. Like this article if it solved a problem? Pass it to a teammate building CLI tools.