Skip to content

Viper 配置管理

CLI 工具通常需要读取配置:数据库连接信息、API 端点、超时时间等。硬编码在代码中不灵活,命令行参数又太多太繁琐。Viper 是 Go 生态中最流行的配置管理库,由 Cobra 的作者 Steve Francia 开发,支持多种配置格式、环境变量、命令行参数的统一管理。本篇将系统讲解 Viper 的核心用法及其与 Cobra 的集成。

一、Viper 简介

Viper 是一个完整的配置解决方案,它的设计目标是"在应用程序中无论配置来源如何,都能以统一的方式访问配置"。Viper 的核心特性包括:

  • 多格式支持:JSON、YAML、TOML、HCL、INI、envfile、Java properties。
  • 多来源合并:默认值 → 配置文件 → 环境变量 → 命令行参数 → 显式 Set,按优先级自动合并。
  • 热更新:监听配置文件变化,运行时自动重新加载。
  • 结构化读取:通过 Unmarshal 将配置映射到结构体。
  • 嵌套访问:通过点号路径 viper.GetString("database.host") 访问嵌套配置。
  • 与 Cobra 无缝集成:一键绑定 flag 到 viper。

Viper 的配置优先级(从高到低):

  1. 显式调用 viper.Set(key, value)
  2. 命令行 flag
  3. 环境变量
  4. 配置文件
  5. 键值存储(etcd/consul,远程配置)
  6. 默认值 viper.SetDefault(key, value)

二、安装与集成

bash
go get github.com/spf13/viper@latest

最小示例:

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 设置默认值
	viper.SetDefault("app_name", "myapp")
	viper.SetDefault("port", 8080)

	// 读取配置
	fmt.Printf("应用名称: %s\n", viper.GetString("app_name"))
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
}

三、配置文件支持

Viper 支持多种配置文件格式。它会根据文件扩展名自动选择解析器。

1. YAML 配置文件

先创建一个 YAML 配置文件 config.yaml

yaml
app_name: myapp
port: 3000
debug: true
database:
  host: localhost
  port: 5432
  name: mydb
  username: admin
  password: secret
features:
  - auth
  - logging
  - cache

读取这个配置文件:

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 设置配置文件名和路径
	viper.SetConfigName("config") // 文件名(不含扩展名)
	viper.SetConfigType("yaml")   // 文件类型
	viper.AddConfigPath(".")      // 搜索路径(可添加多个)
	viper.AddConfigPath("/etc/myapp/")
	viper.AddConfigPath("$HOME/.myapp")

	// 读取配置文件
	if err := viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			fmt.Println("未找到配置文件,使用默认值")
		} else {
			fmt.Printf("读取配置文件失败: %v\n", err)
		}
	} else {
		fmt.Printf("已加载配置文件: %s\n", viper.ConfigFileUsed())
	}

	// 读取配置项
	fmt.Printf("应用名称: %s\n", viper.GetString("app_name"))
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
	fmt.Printf("调试模式: %v\n", viper.GetBool("debug"))

	// 读取嵌套配置(使用点号路径)
	fmt.Printf("数据库主机: %s\n", viper.GetString("database.host"))
	fmt.Printf("数据库端口: %d\n", viper.GetInt("database.port"))
	fmt.Printf("数据库名: %s\n", viper.GetString("database.name"))

	// 读取切片
	features := viper.GetStringSlice("features")
	fmt.Printf("功能列表: %v\n", features)
	for i, f := range features {
		fmt.Printf("  [%d] %s\n", i, f)
	}
}

2. JSON 配置文件

Viper 同样支持 JSON 格式。创建 config.json

json
{
  "app_name": "myapp",
  "port": 3000,
  "debug": true,
  "database": {
    "host": "localhost",
    "port": 5432,
    "name": "mydb"
  }
}

读取方式完全一样,只需修改配置类型:

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config")
	viper.SetConfigType("json")
	viper.AddConfigPath(".")

	if err := viper.ReadInConfig(); err != nil {
		fmt.Printf("读取配置失败: %v\n", err)
		return
	}

	fmt.Printf("应用: %s\n", viper.GetString("app_name"))
	fmt.Printf("数据库: %s:%d/%s\n",
		viper.GetString("database.host"),
		viper.GetInt("database.port"),
		viper.GetString("database.name"),
	)
}

3. TOML 配置文件

创建 config.toml

toml
app_name = "myapp"
port = 3000
debug = true

[database]
host = "localhost"
port = 5432
name = "mydb"
go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config")
	viper.SetConfigType("toml")
	viper.AddConfigPath(".")

	if err := viper.ReadInConfig(); err != nil {
		fmt.Printf("读取配置失败: %v\n", err)
		return
	}

	fmt.Printf("应用: %s (port=%d)\n", viper.GetString("app_name"), viper.GetInt("port"))
	fmt.Printf("数据库: %s:%d\n", viper.GetString("database.host"), viper.GetInt("database.port"))
}

四、环境变量读取

Viper 支持从环境变量读取配置,这在容器化部署中尤为重要(Docker/Kubernetes 常用环境变量传配置)。

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 启用环境变量读取
	viper.AutomaticEnv()

	// 设置环境变量前缀,读取时会自动加上 MYAPP_ 前缀
	// 例如 key 为 "port" 时,会查找环境变量 MYAPP_PORT
	viper.SetEnvPrefix("myapp")

	// 可选:设置环境变量名映射规则
	// 默认会将点号替换为下划线,并转大写
	// database.host -> MYAPP_DATABASE_HOST
	viper.SetEnvKeyReplacer(nil) // 可自定义替换器

	// 设置默认值(当环境变量不存在时使用)
	viper.SetDefault("port", 8080)
	viper.SetDefault("database.host", "localhost")

	// 读取环境变量
	// 如果设置了环境变量 MYAPP_PORT=3000,则返回 3000
	// 否则返回默认值 8080
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
	fmt.Printf("数据库主机: %s\n", viper.GetString("database.host"))
}

运行示例:

bash
# 不设置环境变量,使用默认值
$ go run main.go
端口: 8080
数据库主机: localhost

# 设置环境变量
$ MYAPP_PORT=3000 MYAPP_DATABASE_HOST=db.internal go run main.go
端口: 3000
数据库主机: db.internal

也可以单独绑定某个 key 到特定的环境变量:

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 将 key "database.password" 绑定到环境变量 DB_PASSWORD
	viper.BindEnv("database.password", "DB_PASSWORD")
	viper.SetDefault("database.password", "defaultpass")

	fmt.Printf("数据库密码: %s\n", viper.GetString("database.password"))
}

五、命令行参数绑定

Viper 可以与 Cobra 的 flag 绑定,实现"命令行参数覆盖配置文件"的效果。

go
package main

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

func main() {
	rootCmd := &cobra.Command{
		Use: "myapp",
		Run: func(cmd *cobra.Command, args []string) {
			// 通过 viper 读取配置,自动选择优先级最高的来源
			fmt.Printf("主机: %s\n", viper.GetString("host"))
			fmt.Printf("端口: %d\n", viper.GetInt("port"))
			fmt.Printf("调试: %v\n", viper.GetBool("debug"))
		},
	}

	// 定义 flag
	rootCmd.Flags().String("host", "localhost", "服务器地址")
	rootCmd.Flags().Int("port", 8080, "服务器端口")
	rootCmd.Flags().Bool("debug", false, "调试模式")

	// 将 flag 绑定到 viper
	viper.BindPFlag("host", rootCmd.Flags().Lookup("host"))
	viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
	viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug"))

	rootCmd.Execute()
}

运行示例:

bash
# 使用 flag 默认值
$ go run main.go
主机: localhost
端口: 8080
调试: false

# 命令行覆盖
$ go run main.go --host 0.0.0.0 --port 3000 --debug
主机: 0.0.0.0
端口: 3000
调试: true

六、配置优先级实战

Viper 的核心价值在于统一管理多个配置来源。下面通过一个完整示例演示优先级:

go
package main

import (
	"fmt"
	"os"

	"github.com/spf13/viper"
)

func main() {
	// 1. 黌值(最低优先级)
	viper.SetDefault("host", "0.0.0.0")
	viper.SetDefault("port", 8080)
	viper.SetDefault("debug", false)

	// 2. 配置文件
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	viper.ReadInConfig() // 如果文件不存在,会使用默认值

	// 3. 环境变量
	viper.SetEnvPrefix("myapp")
	viper.AutomaticEnv()

	// 4. 命令行参数(这里用显式 Set 模拟)
	if len(os.Args) > 1 && os.Args[1] == "--port" && len(os.Args) > 2 {
		viper.Set("port", os.Args[2])
	}

	// 5. 显式 Set(最高优先级)
	// viper.Set("port", 9999)

	// 读取最终值(Viper 会按优先级自动选择)
	fmt.Println("=== 最终配置 ===")
	fmt.Printf("host  = %s\n", viper.GetString("host"))
	fmt.Printf("port  = %d\n", viper.GetInt("port"))
	fmt.Printf("debug = %v\n", viper.GetBool("debug"))

	// 打印各来源的值,便于理解优先级
	fmt.Println("\n=== 来源追踪 ===")
	if viper.IsSet("port") {
		fmt.Printf("port 已被设置 (来自: %s)\n", traceSource("port"))
	}
}

func traceSource(key string) string {
	// 检查是否来自环境变量
	if val := os.Getenv("MYAPP_" + toEnvName(key)); val != "" {
		return "环境变量"
	}
	// 检查是否来自配置文件
	if viper.InConfig(key) {
		return "配置文件"
	}
	return "默认值"
}

func toEnvName(s string) string {
	result := ""
	for i, c := range s {
		if i > 0 && c >= 'A' && c <= 'Z' {
			result += "_"
		}
		if c >= 'a' && c <= 'z' {
			c -= 32
		}
		result += string(c)
	}
	return result
}

七、热更新配置:WatchConfig

Viper 支持监听配置文件变化,实现运行时热更新。这在需要不重启服务就调整配置的场景下非常有用。

go
package main

import (
	"fmt"
	"log"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")

	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("读取配置失败: %v", err)
	}

	fmt.Printf("初始端口: %d\n", viper.GetInt("port"))

	// 监听配置文件变化
	viper.OnConfigChange(func(e interface{}) {
		fmt.Println("\n配置文件已变更!")
		fmt.Printf("新端口: %d\n", viper.GetInt("port"))
		fmt.Printf("新调试模式: %v\n", viper.GetBool("debug"))
	})

	viper.WatchConfig()

	// 保持程序运行
	fmt.Println("\n监听配置变化中... 修改 config.yaml 试试")
	select {}
}

运行后修改 config.yaml 文件,Viper 会自动检测到变化并触发回调。

八、配置结构化:Unmarshal

手动调用 viper.GetString("database.host") 在配置项很多时很繁琐。Viper 支持将配置一次性反序列化到结构体。

go
package main

import (
	"fmt"
	"log"

	"github.com/spf13/viper"
)

// Config 对应整个配置文件结构
type Config struct {
	AppName  string   `mapstructure:"app_name"`
	Port     int      `mapstructure:"port"`
	Debug    bool     `mapstructure:"debug"`
	Database Database `mapstructure:"database"`
	Features []string `mapstructure:"features"`
}

// Database 对应 database 配置块
type Database struct {
	Host     string `mapstructure:"host"`
	Port     int    `mapstructure:"port"`
	Name     string `mapstructure:"name"`
	Username string `mapstructure:"username"`
	Password string `mapstructure:"password"`
}

func main() {
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")

	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("读取配置失败: %v", err)
	}

	var cfg Config
	// Unmarshal 将配置映射到结构体
	if err := viper.Unmarshal(&cfg); err != nil {
		log.Fatalf("解析配置失败: %v", err)
	}

	// 使用结构体访问配置,类型安全且有代码补全
	fmt.Printf("应用名称: %s\n", cfg.AppName)
	fmt.Printf("端口: %d\n", cfg.Port)
	fmt.Printf("调试: %v\n", cfg.Debug)
	fmt.Printf("数据库: %s@%s:%d/%s\n",
		cfg.Database.Username,
		cfg.Database.Host,
		cfg.Database.Port,
		cfg.Database.Name,
	)
	fmt.Printf("功能: %v\n", cfg.Features)
}

注意:Viper 使用 mapstructure tag(不是 json tag)进行字段映射。tag 名使用下划线风格(如 app_name)对应 YAML 中的 app_name

九、多配置文件合并

有时需要将配置拆分为多个文件(如基础配置 + 环境特定配置)。Viper 支持通过 MergeInConfig 合并多个配置文件。

go
package main

import (
	"fmt"
	"log"

	"github.com/spf13/viper"
)

func main() {
	// 读取基础配置
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("读取基础配置失败: %v", err)
	}

	fmt.Println("=== 基础配置 ===")
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
	fmt.Printf("数据库主机: %s\n", viper.GetString("database.host"))

	// 读取环境特定配置并合并(覆盖同名 key)
	viper.SetConfigName("config.prod")
	if err := viper.MergeInConfig(); err != nil {
		log.Fatalf("读取生产配置失败: %v", err)
	}

	fmt.Println("\n=== 合并后配置 ===")
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
	fmt.Printf("数据库主机: %s\n", viper.GetString("database.host"))
}

config.yaml(基础):

yaml
port: 8080
database:
  host: localhost
  port: 5432

config.prod.yaml(生产覆盖):

yaml
port: 3000
database:
  host: prod-db.internal

合并后,portdatabase.host 被覆盖,database.port 保持基础值。

十、默认值设置

默认值是最低优先级的配置来源,确保程序在没有任何配置时也能运行。

go
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 设置默认值
	viper.SetDefault("app_name", "myapp")
	viper.SetDefault("port", 8080)
	viper.SetDefault("debug", false)
	viper.SetDefault("max_connections", 100)
	viper.SetDefault("timeout", "30s")

	// 支持嵌套默认值
	viper.SetDefault("database.host", "localhost")
	viper.SetDefault("database.port", 5432)
	viper.SetDefault("database.pool_size", 10)

	// 读取配置(没有配置文件和环境变量,使用默认值)
	fmt.Printf("应用: %s\n", viper.GetString("app_name"))
	fmt.Printf("端口: %d\n", viper.GetInt("port"))
	fmt.Printf("超时: %s\n", viper.GetString("timeout"))
	fmt.Printf("数据库: %s:%d (pool=%d)\n",
		viper.GetString("database.host"),
		viper.GetInt("database.port"),
		viper.GetInt("database.pool_size"),
	)
}

十一、在 Cobra 中集成 Viper

Cobra 和 Viper 由同一作者开发,集成非常顺畅。推荐使用 viper.BindPFlag 将 Cobra 的 flag 绑定到 Viper,实现配置优先级自动管理。

go
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// Config 结构体定义
type Config struct {
	Host     string `mapstructure:"host"`
	Port     int    `mapstructure:"port"`
	Debug    bool   `mapstructure:"debug"`
	LogLevel string `mapstructure:"log_level"`
}

var cfgFile string

func main() {
	rootCmd := &cobra.Command{
		Use:   "myapp",
		Short: "Cobra + Viper 集成示例",
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
			// 在命令执行前初始化配置
			initConfig()
		},
		Run: func(cmd *cobra.Command, args []string) {
			var cfg Config
			if err := viper.Unmarshal(&cfg); err != nil {
				fmt.Fprintf(os.Stderr, "解析配置失败: %v\n", err)
				os.Exit(1)
			}

			fmt.Println("=== 运行配置 ===")
			fmt.Printf("配置文件: %s\n", cfgFile)
			fmt.Printf("主机: %s\n", cfg.Host)
			fmt.Printf("端口: %d\n", cfg.Port)
			fmt.Printf("调试: %v\n", cfg.Debug)
			fmt.Printf("日志级别: %s\n", cfg.LogLevel)
		},
	}

	// 持久标志:配置文件路径
	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "配置文件路径")

	// 普通标志
	rootCmd.Flags().String("host", "localhost", "服务器地址")
	rootCmd.Flags().Int("port", 8080, "服务器端口")
	rootCmd.Flags().Bool("debug", false, "调试模式")
	rootCmd.Flags().String("log-level", "info", "日志级别")

	// 绑定 flag 到 viper
	viper.BindPFlag("host", rootCmd.Flags().Lookup("host"))
	viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
	viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug"))
	viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))

	rootCmd.Execute()
}

// initConfig 初始化配置
func initConfig() {
	// 设置默认值
	viper.SetDefault("host", "localhost")
	viper.SetDefault("port", 8080)
	viper.SetDefault("debug", false)
	viper.SetDefault("log_level", "info")

	// 环境变量
	viper.SetEnvPrefix("myapp")
	viper.AutomaticEnv()

	// 配置文件
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		viper.SetConfigName("config")
		viper.SetConfigType("yaml")
		viper.AddConfigPath(".")
		viper.AddConfigPath("$HOME/.myapp")
		viper.AddConfigPath("/etc/myapp")
	}

	if err := viper.ReadInConfig(); err == nil {
		fmt.Fprintf(os.Stderr, "使用配置文件: %s\n", viper.ConfigFileUsed())
	}
}

十二、完整示例:数据库连接配置工具

下面用一个完整的数据库连接配置工具演示 Viper 的所有核心功能。

go
package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// DatabaseConfig 数据库配置结构
type DatabaseConfig struct {
	Driver          string        `mapstructure:"driver"`
	Host            string        `mapstructure:"host"`
	Port            int           `mapstructure:"port"`
	Username        string        `mapstructure:"username"`
	Password        string        `mapstructure:"password"`
	Name            string        `mapstructure:"name"`
	MaxOpenConns    int           `mapstructure:"max_open_conns"`
	MaxIdleConns    int           `mapstructure:"max_idle_conns"`
	ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
	SSLMode         string        `mapstructure:"ssl_mode"`
}

// AppConfig 应用配置结构
type AppConfig struct {
	Env      string         `mapstructure:"env"`
	Database DatabaseConfig `mapstructure:"database"`
}

var cfgFile string

func main() {
	rootCmd := &cobra.Command{
		Use:   "dbtool",
		Short: "数据库连接配置工具",
		Long:  "一个使用 Viper 管理配置的数据库连接工具示例。",
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return initConfig()
		},
	}

	// 持久标志
	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "配置文件路径")
	rootCmd.PersistentFlags().String("env", "development", "运行环境 (development/staging/production)")

	// 数据库标志
	rootCmd.Flags().String("db-driver", "postgres", "数据库驱动")
	rootCmd.Flags().String("db-host", "localhost", "数据库主机")
	rootCmd.Flags().Int("db-port", 5432, "数据库端口")
	rootCmd.Flags().String("db-user", "admin", "数据库用户名")
	rootCmd.Flags().String("db-pass", "", "数据库密码")
	rootCmd.Flags().String("db-name", "mydb", "数据库名称")
	rootCmd.Flags().Int("db-max-open", 25, "最大连接数")
	rootCmd.Flags().Int("db-max-idle", 10, "最大空闲连接数")
	rootCmd.Flags().String("db-ssl-mode", "disable", "SSL 模式")

	// 绑定 flag 到 viper
	viper.BindPFlag("env", rootCmd.PersistentFlags().Lookup("env"))
	viper.BindPFlag("database.driver", rootCmd.Flags().Lookup("db-driver"))
	viper.BindPFlag("database.host", rootCmd.Flags().Lookup("db-host"))
	viper.BindPFlag("database.port", rootCmd.Flags().Lookup("db-port"))
	viper.BindPFlag("database.username", rootCmd.Flags().Lookup("db-user"))
	viper.BindPFlag("database.password", rootCmd.Flags().Lookup("db-pass"))
	viper.BindPFlag("database.name", rootCmd.Flags().Lookup("db-name"))
	viper.BindPFlag("database.max_open_conns", rootCmd.Flags().Lookup("db-max-open"))
	viper.BindPFlag("database.max_idle_conns", rootCmd.Flags().Lookup("db-max-idle"))
	viper.BindPFlag("database.ssl_mode", rootCmd.Flags().Lookup("db-ssl-mode"))

	// show 子命令:显示当前配置
	showCmd := &cobra.Command{
		Use:   "show",
		Short: "显示当前数据库配置",
		Run: func(cmd *cobra.Command, args []string) {
			var cfg AppConfig
			if err := viper.Unmarshal(&cfg); err != nil {
				fmt.Fprintf(os.Stderr, "解析配置失败: %v\n", err)
				os.Exit(1)
			}
			printConfig(cfg)
		},
	}

	// dsn 子命令:生成连接字符串
	dsnCmd := &cobra.Command{
		Use:   "dsn",
		Short: "生成数据库连接字符串 (DSN)",
		Run: func(cmd *cobra.Command, args []string) {
			var cfg AppConfig
			if err := viper.Unmarshal(&cfg); err != nil {
				fmt.Fprintf(os.Stderr, "解析配置失败: %v\n", err)
				os.Exit(1)
			}
			dsn := generateDSN(cfg.Database)
			fmt.Println(dsn)
		},
	}

	// test 子命令:测试连接(模拟)
	testCmd := &cobra.Command{
		Use:   "test",
		Short: "测试数据库连接",
		Run: func(cmd *cobra.Command, args []string) {
			var cfg AppConfig
			if err := viper.Unmarshal(&cfg); err != nil {
				fmt.Fprintf(os.Stderr, "解析配置失败: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("正在连接 %s://%s:%d/%s ... ", cfg.Database.Driver, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
			fmt.Println("连接成功!")
		},
	}

	rootCmd.AddCommand(showCmd, dsnCmd, testCmd)

	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

// initConfig 初始化 Viper 配置
func initConfig() error {
	// 默认值
	viper.SetDefault("env", "development")
	viper.SetDefault("database.driver", "postgres")
	viper.SetDefault("database.host", "localhost")
	viper.SetDefault("database.port", 5432)
	viper.SetDefault("database.max_open_conns", 25)
	viper.SetDefault("database.max_idle_conns", 10)
	viper.SetDefault("database.conn_max_lifetime", "5m")
	viper.SetDefault("database.ssl_mode", "disable")

	// 环境变量
	viper.SetEnvPrefix("dbtool")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	viper.AutomaticEnv()

	// 配置文件
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		viper.SetConfigName("config")
		viper.SetConfigType("yaml")
		viper.AddConfigPath(".")
		viper.AddConfigPath("$HOME/.dbtool")
		viper.AddConfigPath("/etc/dbtool")
	}

	if err := viper.ReadInConfig(); err == nil {
		fmt.Fprintf(os.Stderr, "[dbtool] 加载配置文件: %s\n", viper.ConfigFileUsed())
	} else {
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			return fmt.Errorf("读取配置文件失败: %w", err)
		}
	}

	return nil
}

// printConfig 打印配置
func printConfig(cfg AppConfig) {
	fmt.Println("========================================")
	fmt.Printf("  运行环境: %s\n", cfg.Env)
	fmt.Println("========================================")
	fmt.Println("数据库配置:")
	fmt.Printf("  驱动:       %s\n", cfg.Database.Driver)
	fmt.Printf("  主机:       %s\n", cfg.Database.Host)
	fmt.Printf("  端口:       %d\n", cfg.Database.Port)
	fmt.Printf("  用户名:     %s\n", cfg.Database.Username)
	fmt.Printf("  密码:       %s\n", maskPassword(cfg.Database.Password))
	fmt.Printf("  数据库名:   %s\n", cfg.Database.Name)
	fmt.Printf("  SSL 模式:   %s\n", cfg.Database.SSLMode)
	fmt.Printf("  最大连接数: %d\n", cfg.Database.MaxOpenConns)
	fmt.Printf("  空闲连接数: %d\n", cfg.Database.MaxIdleConns)
	fmt.Println("========================================")
}

// maskPassword 密码脱敏
func maskPassword(pwd string) string {
	if pwd == "" {
		return "(未设置)"
	}
	if len(pwd) <= 2 {
		return strings.Repeat("*", len(pwd))
	}
	return string(pwd[0]) + strings.Repeat("*", len(pwd)-2) + string(pwd[len(pwd)-1])
}

// generateDSN 生成连接字符串
func generateDSN(db DatabaseConfig) string {
	switch db.Driver {
	case "postgres":
		return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
			db.Host, db.Port, db.Username, db.Password, db.Name, db.SSLMode)
	case "mysql":
		return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true",
			db.Username, db.Password, db.Host, db.Port, db.Name)
	default:
		return fmt.Sprintf("%s://%s:%s@%s:%d/%s", db.Driver, db.Username, db.Password, db.Host, db.Port, db.Name)
	}
}

运行示例:

bash
# 使用默认配置
$ go run main.go show
========================================
  运行环境: development
========================================
数据库配置:
  驱动:       postgres
  主机:       localhost
  端口:       5432
  ...
========================================

# 命令行覆盖配置
$ go run main.go show --env production --db-host prod-db.internal --db-port 5433
[dbtool] 加载配置文件: ./config.yaml
========================================
  运行环境: production
========================================
数据库配置:
  驱动:       postgres
  主机:       prod-db.internal
  端口:       5433
  ...

# 环境变量覆盖
$ DBTOOL_DATABASE_HOST=env-db.internal go run main.go show

# 生成 DSN
$ go run main.go dsn --db-host db.local --db-user root --db-pass secret
host=db.local port=5432 user=root password=secret dbname=mydb sslmode=disable

# 测试连接
$ go run main.go test
正在连接 postgres://localhost:5432/mydb ... 连接成功!

十三、小结

本篇系统学习了 Viper 配置管理的核心用法:

  1. Viper 简介:统一的配置管理方案,支持多格式、多来源、热更新。
  2. 配置文件:支持 YAML、JSON、TOML 等,通过 SetConfigName + AddConfigPath 搜索。
  3. 环境变量:通过 AutomaticEnv + SetEnvPrefix 自动映射,也支持 BindEnv 单独绑定。
  4. 命令行参数绑定:通过 BindPFlag 将 Cobra flag 绑定到 Viper。
  5. 配置优先级:显式 Set > flag > 环境变量 > 配置文件 > 默认值。
  6. 热更新WatchConfig + OnConfigChange 实现运行时配置重载。
  7. 结构化读取Unmarshal 将配置映射到结构体,类型安全。
  8. 多配置合并MergeInConfig 合并多个配置文件。
  9. 默认值SetDefault 确保程序无配置时也能运行。
  10. Cobra 集成:通过 BindPFlag 打通 flag 与配置,实现优先级自动管理。
  11. 完整示例:数据库配置工具演示了多来源配置、结构化读取、子命令分发。

Cobra + Viper 是 Go CLI 开发的黄金组合。下一篇我们将进入交互式 CLI 的世界,学习如何让 CLI 工具更加用户友好。