cli-config

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CLI 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):
  1. Explicit
    viper.Set()
    calls
  2. Command-line flags
  3. Environment variables
  4. Config file values
  5. 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 set
Viper使用以下优先级顺序(从高到低):
  1. 显式
    viper.Set()
    调用
  2. 命令行标志
  3. 环境变量
  4. 配置文件值
  5. 默认值
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 set

Basic 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.name
Custom 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.password

Read 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
    - feature2
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
    - feature2

JSON

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 config
go
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")

// Now MYAPP_SERVER_PORT overrides config

3. 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 config
myapp config init      # Create default config
myapp config show      # Show current config
myapp config validate  # Validate config

5. 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

资源