cli-config
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCLI Configuration with Cobra & Viper
使用Cobra & Viper实现CLI配置
Build flexible, hierarchical configuration systems for CLI applications using Cobra (commands/flags) and Viper (config management).
使用Cobra(命令/标志)和Viper(配置管理)为CLI应用构建灵活的分层配置系统。
Your Role: Configuration Architect
你的角色:配置架构师
You design configuration systems with proper precedence and flexibility. You:
✅ Implement config hierarchy - Flags > Env > Config > Defaults
✅ Bind flags to Viper - Seamless integration
✅ Support multiple formats - YAML, JSON, TOML
✅ Handle environment variables - With prefixes
✅ Provide config commands - init, show, validate
✅ Follow CLY patterns - Use project structure
❌ Do NOT hardcode paths - Use conventions
❌ Do NOT skip validation - Validate config
❌ Do NOT ignore precedence - Follow hierarchy
你需要设计具备适当优先级和灵活性的配置系统。你需要:
✅ 实现配置层级 - 标志 > 环境变量 > 配置文件 > 默认值
✅ 将标志绑定到Viper - 无缝集成
✅ 支持多种格式 - YAML、JSON、TOML
✅ 处理环境变量 - 带前缀
✅ 提供配置命令 - init、show、validate
✅ 遵循CLY模式 - 使用项目结构
❌ 请勿硬编码路径 - 使用约定
❌ 请勿跳过验证 - 验证配置
❌ 请勿忽略优先级 - 遵循层级
Configuration Precedence
配置优先级
Viper uses this precedence order (highest to lowest):
- Explicit calls
viper.Set() - Command-line flags
- Environment variables
- Config file values
- Defaults
go
viper.SetDefault("port", 8080) // 5. Default
// config.yaml: port: 8081 // 4. Config file
os.Setenv("APP_PORT", "8082") // 3. Environment
cobra.Flags().Int("port", 0, "Port") // 2. Flag
viper.Set("port", 8083) // 1. Explicit setViper使用以下优先级顺序(从高到低):
- 显式调用
viper.Set() - 命令行标志
- 环境变量
- 配置文件值
- 默认值
go
viper.SetDefault("port", 8080) // 5. Default
// config.yaml: port: 8081 // 4. Config file
os.Setenv("APP_PORT", "8082") // 3. Environment
cobra.Flags().Int("port", 0, "Port") // 2. Flag
viper.Set("port", 8083) // 1. Explicit setBasic Setup
基础设置
Initialize Viper
初始化Viper
go
package config
import (
"fmt"
"os"
"github.com/spf13/viper"
)
func Init() error {
// Set config name (no extension)
viper.SetConfigName("config")
// Set config type
viper.SetConfigType("yaml")
// Add search paths
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath("/etc/myapp")
// Read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; use defaults
return nil
}
return fmt.Errorf("error reading config: %w", err)
}
return nil
}go
package config
import (
"fmt"
"os"
"github.com/spf13/viper"
)
func Init() error {
// Set config name (no extension)
viper.SetConfigName("config")
// Set config type
viper.SetConfigType("yaml")
// Add search paths
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath("/etc/myapp")
// Read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; use defaults
return nil
}
return fmt.Errorf("error reading config: %w", err)
}
return nil
}With Cobra Integration
与Cobra集成
go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Global flags
rootCmd.PersistentFlags().StringVar(
&cfgFile,
"config",
"",
"config file (default is $HOME/.myapp/config.yaml)",
)
}
func initConfig() {
if cfgFile != "" {
// Use explicit config file
viper.SetConfigFile(cfgFile)
} else {
// Find home directory
home, err := os.UserHomeDir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory and current directory
viper.AddConfigPath(home + "/.myapp")
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
// Read environment variables
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Read config file
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Global flags
rootCmd.PersistentFlags().StringVar(
&cfgFile,
"config",
"",
"config file (default is $HOME/.myapp/config.yaml)",
)
}
func initConfig() {
if cfgFile != "" {
// Use explicit config file
viper.SetConfigFile(cfgFile)
} else {
// Find home directory
home, err := os.UserHomeDir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory and current directory
viper.AddConfigPath(home + "/.myapp")
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
// Read environment variables
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Read config file
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}Configuration Patterns
配置模式
Set Defaults
设置默认值
go
func setDefaults() {
// Server
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.timeout", "30s")
// Database
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
viper.SetDefault("database.name", "myapp")
// Logging
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "json")
}go
func setDefaults() {
// Server
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.timeout", "30s")
// Database
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
viper.SetDefault("database.name", "myapp")
// Logging
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "json")
}Bind Flags
绑定标志
Single flag:
go
cmd.Flags().IntP("port", "p", 8080, "Port to run on")
viper.BindPFlag("server.port", cmd.Flags().Lookup("port"))All flags:
go
cmd.Flags().Int("port", 8080, "Port")
cmd.Flags().String("host", "localhost", "Host")
viper.BindPFlags(cmd.Flags())Persistent flags:
go
rootCmd.PersistentFlags().String("log-level", "info", "Log level")
viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))单个标志:
go
cmd.Flags().IntP("port", "p", 8080, "Port to run on")
viper.BindPFlag("server.port", cmd.Flags().Lookup("port"))所有标志:
go
cmd.Flags().Int("port", 8080, "Port")
cmd.Flags().String("host", "localhost", "Host")
viper.BindPFlags(cmd.Flags())持久标志:
go
rootCmd.PersistentFlags().String("log-level", "info", "Log level")
viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))Environment Variables
环境变量
Auto-map all env vars:
go
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port
// MYAPP_DATABASE_NAME → database.nameCustom env key replacer:
go
import "strings"
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port (. → _)Bind specific env var:
go
viper.BindEnv("database.password", "DB_PASSWORD")
// DB_PASSWORD → database.password自动映射所有环境变量:
go
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port
// MYAPP_DATABASE_NAME → database.name自定义环境变量键替换器:
go
import "strings"
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port (. → _)绑定特定环境变量:
go
viper.BindEnv("database.password", "DB_PASSWORD")
// DB_PASSWORD → database.passwordRead Config Values
读取配置值
Get typed values:
go
port := viper.GetInt("server.port")
host := viper.GetString("server.host")
enabled := viper.GetBool("feature.enabled")
timeout := viper.GetDuration("server.timeout")
tags := viper.GetStringSlice("tags")Check if set:
go
if viper.IsSet("server.port") {
port := viper.GetInt("server.port")
}Get with default:
go
port := viper.GetInt("server.port")
if port == 0 {
port = 8080
}获取类型化值:
go
port := viper.GetInt("server.port")
host := viper.GetString("server.host")
enabled := viper.GetBool("feature.enabled")
timeout := viper.GetDuration("server.timeout")
tags := viper.GetStringSlice("tags")检查是否已设置:
go
if viper.IsSet("server.port") {
port := viper.GetInt("server.port")
}带默认值获取:
go
port := viper.GetInt("server.port")
if port == 0 {
port = 8080
}Unmarshal to Struct
反序列化为结构体
Full config:
go
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
Timeout string `mapstructure:"timeout"`
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}Subsection:
go
var serverConfig ServerConfig
if err := viper.UnmarshalKey("server", &serverConfig); err != nil {
return fmt.Errorf("unable to decode server config: %w", err)
}完整配置:
go
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
Timeout string `mapstructure:"timeout"`
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}子配置:
go
var serverConfig ServerConfig
if err := viper.UnmarshalKey("server", &serverConfig); err != nil {
return fmt.Errorf("unable to decode server config: %w", err)
}Write Config
写入配置
Create default config:
go
func createDefaultConfig(path string) error {
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
return viper.WriteConfigAs(path)
}Save current config:
go
viper.Set("server.port", 9090)
// Write to current config file
viper.WriteConfig()
// Write to specific file
viper.WriteConfigAs("/path/to/config.yaml")
// Safe write (won't overwrite)
viper.SafeWriteConfig()创建默认配置:
go
func createDefaultConfig(path string) error {
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
return viper.WriteConfigAs(path)
}保存当前配置:
go
viper.Set("server.port", 9090)
// Write to current config file
viper.WriteConfig()
// Write to specific file
viper.WriteConfigAs("/path/to/config.yaml")
// Safe write (won't overwrite)
viper.SafeWriteConfig()CLY Project Pattern
CLY项目模式
Config Package
配置包
pkg/config/config.go:
go
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
var cfg *Config
// Init initializes the configuration
func Init(cfgFile string) error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
return err
}
viper.AddConfigPath(filepath.Join(home, ".cly"))
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
setDefaults()
viper.AutomaticEnv()
viper.SetEnvPrefix("CLY")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
cfg = &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}
return nil
}
func setDefaults() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "text")
}
// Get returns the current config
func Get() *Config {
return cfg
}
// GetString returns a config value as string
func GetString(key string) string {
return viper.GetString(key)
}
// GetInt returns a config value as int
func GetInt(key string) int {
return viper.GetInt(key)
}
// GetBool returns a config value as bool
func GetBool(key string) bool {
return viper.GetBool(key)
}pkg/config/config.go:
go
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
var cfg *Config
// Init initializes the configuration
func Init(cfgFile string) error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
return err
}
viper.AddConfigPath(filepath.Join(home, ".cly"))
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
setDefaults()
viper.AutomaticEnv()
viper.SetEnvPrefix("CLY")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
cfg = &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}
return nil
}
func setDefaults() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "text")
}
// Get returns the current config
func Get() *Config {
return cfg
}
// GetString returns a config value as string
func GetString(key string) string {
return viper.GetString(key)
}
// GetInt returns a config value as int
func GetInt(key string) int {
return viper.GetInt(key)
}
// GetBool returns a config value as bool
func GetBool(key string) bool {
return viper.GetBool(key)
}Root Command Integration
根命令集成
cmd/root.go:
go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/yurifrl/cly/pkg/config"
)
var cfgFile string
var RootCmd = &cobra.Command{
Use: "cly",
Short: "CLY - Command Line Yuri",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return config.Init(cfgFile)
},
}
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
RootCmd.PersistentFlags().StringVar(
&cfgFile,
"config",
"",
"config file (default is $HOME/.cly/config.yaml)",
)
}cmd/root.go:
go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/yurifrl/cly/pkg/config"
)
var cfgFile string
var RootCmd = &cobra.Command{
Use: "cly",
Short: "CLY - Command Line Yuri",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return config.Init(cfgFile)
},
}
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
RootCmd.PersistentFlags().StringVar(
&cfgFile,
"config",
"",
"config file (default is $HOME/.cly/config.yaml)",
)
}Config Command
配置命令
modules/config/cmd.go:
go
package configcmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
cmd.AddCommand(
initCmd(),
showCmd(),
validateCmd(),
)
parent.AddCommand(cmd)
}
func initCmd() *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Initialize config file",
RunE: func(cmd *cobra.Command, args []string) error {
path, _ := cmd.Flags().GetString("path")
if path == "" {
path = "$HOME/.cly/config.yaml"
}
if err := viper.SafeWriteConfigAs(path); err != nil {
return fmt.Errorf("failed to create config: %w", err)
}
fmt.Printf("Config created at: %s\n", path)
return nil
},
}
}
func showCmd() *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "Show current configuration",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Current configuration:")
fmt.Println("Config file:", viper.ConfigFileUsed())
fmt.Println()
for _, key := range viper.AllKeys() {
fmt.Printf("%s: %v\n", key, viper.Get(key))
}
return nil
},
}
}
func validateCmd() *cobra.Command {
return &cobra.Command{
Use: "validate",
Short: "Validate configuration",
RunE: func(cmd *cobra.Command, args []string) error {
// Add validation logic
fmt.Println("Configuration is valid")
return nil
},
}
}modules/config/cmd.go:
go
package configcmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
cmd.AddCommand(
initCmd(),
showCmd(),
validateCmd(),
)
parent.AddCommand(cmd)
}
func initCmd() *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Initialize config file",
RunE: func(cmd *cobra.Command, args []string) error {
path, _ := cmd.Flags().GetString("path")
if path == "" {
path = "$HOME/.cly/config.yaml"
}
if err := viper.SafeWriteConfigAs(path); err != nil {
return fmt.Errorf("failed to create config: %w", err)
}
fmt.Printf("Config created at: %s\n", path)
return nil
},
}
}
func showCmd() *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "Show current configuration",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Current configuration:")
fmt.Println("Config file:", viper.ConfigFileUsed())
fmt.Println()
for _, key := range viper.AllKeys() {
fmt.Printf("%s: %v\n", key, viper.Get(key))
}
return nil
},
}
}
func validateCmd() *cobra.Command {
return &cobra.Command{
Use: "validate",
Short: "Validate configuration",
RunE: func(cmd *cobra.Command, args []string) error {
// Add validation logic
fmt.Println("Configuration is valid")
return nil
},
}
}Advanced Patterns
高级模式
Remote Config (etcd, Consul)
远程配置(etcd、Consul)
go
import _ "github.com/spf13/viper/remote"
func initRemoteConfig() error {
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/myapp.json")
viper.SetConfigType("json")
if err := viper.ReadRemoteConfig(); err != nil {
return err
}
return nil
}
// Watch for changes
func watchRemoteConfig() {
go func() {
for {
time.Sleep(time.Second * 5)
err := viper.WatchRemoteConfig()
if err != nil {
log.Printf("unable to read remote config: %v", err)
continue
}
}
}()
}go
import _ "github.com/spf13/viper/remote"
func initRemoteConfig() error {
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/myapp.json")
viper.SetConfigType("json")
if err := viper.ReadRemoteConfig(); err != nil {
return err
}
return nil
}
// Watch for changes
func watchRemoteConfig() {
go func() {
for {
time.Sleep(time.Second * 5)
err := viper.WatchRemoteConfig()
if err != nil {
log.Printf("unable to read remote config: %v", err)
continue
}
}
}()
}Watch Config File
监听配置文件
go
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Reload config
var newConfig Config
if err := viper.Unmarshal(&newConfig); err != nil {
log.Printf("error reloading config: %v", err)
return
}
// Update application state
updateAppConfig(newConfig)
})go
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Reload config
var newConfig Config
if err := viper.Unmarshal(&newConfig); err != nil {
log.Printf("error reloading config: %v", err)
return
}
// Update application state
updateAppConfig(newConfig)
})Multiple Config Instances
多配置实例
go
// Default instance
viper.SetConfigName("config")
viper.ReadInConfig()
// Custom instance
v := viper.New()
v.SetConfigName("other-config")
v.AddConfigPath(".")
v.ReadInConfig()
port := v.GetInt("port")go
// Default instance
viper.SetConfigName("config")
viper.ReadInConfig()
// Custom instance
v := viper.New()
v.SetConfigName("other-config")
v.AddConfigPath(".")
v.ReadInConfig()
port := v.GetInt("port")Config with Validation
带验证的配置
go
type Config struct {
Server ServerConfig `mapstructure:"server" validate:"required"`
DB DBConfig `mapstructure:"database" validate:"required"`
}
type ServerConfig struct {
Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
Host string `mapstructure:"host" validate:"required,hostname"`
}
func Load() (*Config, error) {
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
// Validate
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}go
type Config struct {
Server ServerConfig `mapstructure:"server" validate:"required"`
DB DBConfig `mapstructure:"database" validate:"required"`
}
type ServerConfig struct {
Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
Host string `mapstructure:"host" validate:"required,hostname"`
}
func Load() (*Config, error) {
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
// Validate
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}Nested Config Keys
嵌套配置键
go
// Dot notation
viper.Set("server.database.host", "localhost")
// Nested maps
viper.Set("server", map[string]interface{}{
"database": map[string]interface{}{
"host": "localhost",
"port": 5432,
},
})
// Access nested
host := viper.GetString("server.database.host")
// Get sub-tree
dbConfig := viper.Sub("server.database")
if dbConfig != nil {
host := dbConfig.GetString("host")
}go
// Dot notation
viper.Set("server.database.host", "localhost")
// Nested maps
viper.Set("server", map[string]interface{}{
"database": map[string]interface{}{
"host": "localhost",
"port": 5432,
},
})
// Access nested
host := viper.GetString("server.database.host")
// Get sub-tree
dbConfig := viper.Sub("server.database")
if dbConfig != nil {
host := dbConfig.GetString("host")
}Config File Formats
配置文件格式
YAML
YAML
config.yaml:
yaml
server:
port: 8080
host: localhost
timeout: 30s
database:
host: localhost
port: 5432
name: myapp
user: postgres
password: secret
log:
level: info
format: json
output: stdout
features:
enabled:
- feature1
- feature2config.yaml:
yaml
server:
port: 8080
host: localhost
timeout: 30s
database:
host: localhost
port: 5432
name: myapp
user: postgres
password: secret
log:
level: info
format: json
output: stdout
features:
enabled:
- feature1
- feature2JSON
JSON
config.json:
json
{
"server": {
"port": 8080,
"host": "localhost"
},
"database": {
"host": "localhost",
"port": 5432
}
}config.json:
json
{
"server": {
"port": 8080,
"host": "localhost"
},
"database": {
"host": "localhost",
"port": 5432
}
}TOML
TOML
config.toml:
toml
[server]
port = 8080
host = "localhost"
[database]
host = "localhost"
port = 5432
name = "myapp"config.toml:
toml
[server]
port = 8080
host = "localhost"
[database]
host = "localhost"
port = 5432
name = "myapp"Best Practices
最佳实践
1. Always Set Defaults
1. 始终设置默认值
go
func init() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")
}go
func init() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")
}2. Use Environment Variables
2. 使用环境变量
go
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Now MYAPP_SERVER_PORT overrides configgo
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Now MYAPP_SERVER_PORT overrides config3. Validate Config
3. 验证配置
go
type Config struct {
Port int `validate:"required,min=1,max=65535"`
}
if err := validate.Struct(cfg); err != nil {
return err
}go
type Config struct {
Port int `validate:"required,min=1,max=65535"`
}
if err := validate.Struct(cfg); err != nil {
return err
}4. Provide Config Commands
4. 提供配置命令
myapp config init # Create default config
myapp config show # Show current config
myapp config validate # Validate configmyapp config init # Create default config
myapp config show # Show current config
myapp config validate # Validate config5. Handle Missing Config Gracefully
5. 优雅处理缺失的配置
go
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config not found, use defaults
log.Println("No config file found, using defaults")
} else {
return err
}
}go
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config not found, use defaults
log.Println("No config file found, using defaults")
} else {
return err
}
}6. Don't Store Secrets in Config
6. 不要在配置中存储密钥
go
// ❌ BAD
database:
password: "mysecret"
// ✅ GOOD - Use env vars
database:
password: ${DB_PASSWORD}
// Or
viper.BindEnv("database.password", "DB_PASSWORD")go
// ❌ BAD
database:
password: "mysecret"
// ✅ GOOD - Use env vars
database:
password: ${DB_PASSWORD}
// Or
viper.BindEnv("database.password", "DB_PASSWORD")7. Use Struct Tags
7. 使用结构体标签
go
type ServerConfig struct {
Port int `mapstructure:"port" json:"port" yaml:"port"`
Host string `mapstructure:"host" json:"host" yaml:"host"`
Timeout string `mapstructure:"timeout" json:"timeout" yaml:"timeout"`
}go
type ServerConfig struct {
Port int `mapstructure:"port" json:"port" yaml:"port"`
Host string `mapstructure:"host" json:"host" yaml:"host"`
Timeout string `mapstructure:"timeout" json:"timeout" yaml:"timeout"`
}Common Patterns
常见模式
Config Init Command
配置初始化命令
go
func initConfigCmd() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "init",
Short: "Initialize configuration",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
if configPath == "" {
configPath = filepath.Join(os.Getenv("HOME"), ".myapp", "config.yaml")
}
// Check if exists
if _, err := os.Stat(configPath); err == nil && !force {
return fmt.Errorf("config already exists: %s (use --force to overwrite)", configPath)
}
// Create directory
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return err
}
// Write config
if err := viper.WriteConfigAs(configPath); err != nil {
return err
}
fmt.Printf("Config initialized: %s\n", configPath)
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing config")
return cmd
}go
func initConfigCmd() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "init",
Short: "Initialize configuration",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
if configPath == "" {
configPath = filepath.Join(os.Getenv("HOME"), ".myapp", "config.yaml")
}
// Check if exists
if _, err := os.Stat(configPath); err == nil && !force {
return fmt.Errorf("config already exists: %s (use --force to overwrite)", configPath)
}
// Create directory
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return err
}
// Write config
if err := viper.WriteConfigAs(configPath); err != nil {
return err
}
fmt.Printf("Config initialized: %s\n", configPath)
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing config")
return cmd
}Config Migration
配置迁移
go
func migrateConfig() error {
version := viper.GetInt("version")
switch version {
case 0:
// Migrate from v0 to v1
viper.Set("new_field", "default")
viper.Set("version", 1)
fallthrough
case 1:
// Migrate from v1 to v2
viper.Set("another_field", true)
viper.Set("version", 2)
}
return viper.WriteConfig()
}go
func migrateConfig() error {
version := viper.GetInt("version")
switch version {
case 0:
// Migrate from v0 to v1
viper.Set("new_field", "default")
viper.Set("version", 1)
fallthrough
case 1:
// Migrate from v1 to v2
viper.Set("another_field", true)
viper.Set("version", 2)
}
return viper.WriteConfig()
}Testing
测试
go
func TestConfig(t *testing.T) {
// Use separate viper instance
v := viper.New()
v.SetConfigType("yaml")
var yamlConfig = []byte(`
server:
port: 8080
host: localhost
`)
v.ReadConfig(bytes.NewBuffer(yamlConfig))
assert.Equal(t, 8080, v.GetInt("server.port"))
assert.Equal(t, "localhost", v.GetString("server.host"))
}go
func TestConfig(t *testing.T) {
// Use separate viper instance
v := viper.New()
v.SetConfigType("yaml")
var yamlConfig = []byte(`
server:
port: 8080
host: localhost
`)
v.ReadConfig(bytes.NewBuffer(yamlConfig))
assert.Equal(t, 8080, v.GetInt("server.port"))
assert.Equal(t, "localhost", v.GetString("server.host"))
}Checklist
检查清单
- Defaults set for all config values
- Config file search paths defined
- Environment variable support
- Flags bound to config
- Config struct with mapstructure tags
- Config validation
- Config commands (init, show, validate)
- Error handling for missing config
- Secrets via env vars only
- Config file format documented
- 为所有配置值设置默认值
- 定义配置文件搜索路径
- 支持环境变量
- 将标志绑定到配置
- 带mapstructure标签的配置结构体
- 配置验证
- 配置命令(init、show、validate)
- 缺失配置的错误处理
- 仅通过环境变量存储密钥
- 文档化配置文件格式
Resources
资源
- Viper Documentation
- Cobra User Guide
- 12-Factor Config
- CLY config: ,
pkg/config/modules/config/