3.7 KiB
Cobra + Viper Integration Pattern
This document explains how Cobra (CLI framework) and Viper (configuration management) are integrated in the generated Go CLI projects.
Architecture Overview
The integration follows these principles:
-
Configuration Priority (highest to lowest):
- Command-line flags
- Environment variables
- Config file values
- Default values
-
Lazy Loading: Configuration is loaded once in
PersistentPreRun, before any command executes -
Centralized Access: The
GetConfig()andGetLogger()functions incmd/root.goprovide access to configuration and logging
Key Components
Root Command (cmd/root.go)
The root command sets up the entire configuration system:
var rootCmd = &cobra.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
setupLogging()
},
}
Configuration Initialization (initConfig())
This function:
- Determines config file location (from flag or default)
- Sets default values
- Enables environment variable reading
- Reads the config file (if it exists)
Flag Binding
Flags are bound to Viper keys using viper.BindPFlag():
rootCmd.PersistentFlags().StringP("verbose", "v", false, "verbose output")
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
This creates the hierarchy: CLI flag → Viper key → Config struct
Adding New Configuration
To add a new configuration option:
-
Add to config struct (
internal/config/config.go):type Config struct { MyNewOption string } -
Add flag (
cmd/root.goor command-specific file):rootCmd.PersistentFlags().String("my-option", "default", "description") viper.BindPFlag("my_option", rootCmd.PersistentFlags().Lookup("my-option")) -
Set default (
cmd/root.goininitConfig()):viper.SetDefault("my_option", "default_value") -
Add to config example (
.yaml.example):my_option: "default_value" -
Access in commands:
cfg := GetConfig() value := cfg.MyNewOption // or directly from viper: value := viper.GetString("my_option")
Command-Specific Configuration
For configuration specific to a single command:
-
Add the flag to the command's
init()function, not the root command -
Use a nested structure in the config struct:
type Config struct { Fetch struct { Concurrency int Timeout time.Duration } } -
Bind with a namespaced key:
viper.BindPFlag("fetch.concurrency", fetchCmd.Flags().Lookup("concurrency"))
Environment Variables
Viper automatically maps environment variables when you call viper.AutomaticEnv().
By default, environment variables are matched by converting the key to uppercase and replacing . with _:
- Config key:
fetch.concurrency - Environment variable:
FETCH_CONCURRENCY
Best Practices
-
Use PersistentFlags for global options: Options that apply to all commands should be on
rootCmd.PersistentFlags() -
Use command-specific Flags for local options: Options specific to one command should be on that command's
Flags() -
Provide sensible defaults: Always set defaults in
initConfig()so the tool works without a config file -
Document in .yaml.example: Keep the example config file up to date
-
Keep flag names kebab-case: Use hyphens in CLI flags (
--my-option) and underscores in Viper keys (my_option) -
Use GetConfig() for structured access: Prefer accessing configuration through the typed Config struct rather than calling viper.Get* directly in commands