Appearance
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 的配置优先级(从高到低):
- 显式调用
viper.Set(key, value) - 命令行 flag
- 环境变量
- 配置文件
- 键值存储(etcd/consul,远程配置)
- 默认值
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 使用
mapstructuretag(不是jsontag)进行字段映射。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: 5432config.prod.yaml(生产覆盖):
yaml
port: 3000
database:
host: prod-db.internal合并后,port 和 database.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 配置管理的核心用法:
- Viper 简介:统一的配置管理方案,支持多格式、多来源、热更新。
- 配置文件:支持 YAML、JSON、TOML 等,通过
SetConfigName+AddConfigPath搜索。 - 环境变量:通过
AutomaticEnv+SetEnvPrefix自动映射,也支持BindEnv单独绑定。 - 命令行参数绑定:通过
BindPFlag将 Cobra flag 绑定到 Viper。 - 配置优先级:显式 Set > flag > 环境变量 > 配置文件 > 默认值。
- 热更新:
WatchConfig+OnConfigChange实现运行时配置重载。 - 结构化读取:
Unmarshal将配置映射到结构体,类型安全。 - 多配置合并:
MergeInConfig合并多个配置文件。 - 默认值:
SetDefault确保程序无配置时也能运行。 - Cobra 集成:通过
BindPFlag打通 flag 与配置,实现优先级自动管理。 - 完整示例:数据库配置工具演示了多来源配置、结构化读取、子命令分发。
Cobra + Viper 是 Go CLI 开发的黄金组合。下一篇我们将进入交互式 CLI 的世界,学习如何让 CLI 工具更加用户友好。