Files
gh-lmorchard-lmorchard-agen…/skills/go-cli-builder/references/cobra-viper-integration.md
2025-11-30 08:37:58 +08:00

130 lines
3.7 KiB
Markdown

# 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:
1. **Configuration Priority** (highest to lowest):
- Command-line flags
- Environment variables
- Config file values
- Default values
2. **Lazy Loading**: Configuration is loaded once in `PersistentPreRun`, before any command executes
3. **Centralized Access**: The `GetConfig()` and `GetLogger()` functions in `cmd/root.go` provide access to configuration and logging
## Key Components
### Root Command (`cmd/root.go`)
The root command sets up the entire configuration system:
```go
var rootCmd = &cobra.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
setupLogging()
},
}
```
### Configuration Initialization (`initConfig()`)
This function:
1. Determines config file location (from flag or default)
2. Sets default values
3. Enables environment variable reading
4. Reads the config file (if it exists)
### Flag Binding
Flags are bound to Viper keys using `viper.BindPFlag()`:
```go
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:
1. **Add to config struct** (`internal/config/config.go`):
```go
type Config struct {
MyNewOption string
}
```
2. **Add flag** (`cmd/root.go` or command-specific file):
```go
rootCmd.PersistentFlags().String("my-option", "default", "description")
viper.BindPFlag("my_option", rootCmd.PersistentFlags().Lookup("my-option"))
```
3. **Set default** (`cmd/root.go` in `initConfig()`):
```go
viper.SetDefault("my_option", "default_value")
```
4. **Add to config example** (`.yaml.example`):
```yaml
my_option: "default_value"
```
5. **Access in commands**:
```go
cfg := GetConfig()
value := cfg.MyNewOption
// or directly from viper:
value := viper.GetString("my_option")
```
## Command-Specific Configuration
For configuration specific to a single command:
1. Add the flag to the command's `init()` function, not the root command
2. Use a nested structure in the config struct:
```go
type Config struct {
Fetch struct {
Concurrency int
Timeout time.Duration
}
}
```
3. Bind with a namespaced key:
```go
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
1. **Use PersistentFlags for global options**: Options that apply to all commands should be on `rootCmd.PersistentFlags()`
2. **Use command-specific Flags for local options**: Options specific to one command should be on that command's `Flags()`
3. **Provide sensible defaults**: Always set defaults in `initConfig()` so the tool works without a config file
4. **Document in .yaml.example**: Keep the example config file up to date
5. **Keep flag names kebab-case**: Use hyphens in CLI flags (`--my-option`) and underscores in Viper keys (`my_option`)
6. **Use GetConfig() for structured access**: Prefer accessing configuration through the typed Config struct rather than calling viper.Get* directly in commands