学完这篇,你能用 Viper 从 YAML 配置文件加载参数、让同名环境变量自动覆盖配置,并在配置文件改动后热加载生效,不用重启服务。
第一步:安装依赖并建立最小配置结构
go get github.com/spf13/viper
在项目根目录建 config.yaml:
server:
port: 8080
mode: debug
database:
host: 127.0.0.1
port: 3306
name: app
Viper 的键用点号表示层级,比如 server.port。记住这个写法,后面绑环境变量全靠它。
第二步:加载配置文件
v := viper.New()
v.SetConfigFile("config.yaml") // 指定绝对/相对路径,最省心
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
log.Fatalf("读取配置失败: %v", err)
}
log.Println(v.GetInt("server.port")) // 8080
也可以用 SetConfigName("config") + AddConfigPath("./") 让 Viper 自己在目录里找,但那样会把 config.json、config.toml 也一起找进来,结果不确定。单机项目建议直接 SetConfigFile。
注意:SetConfigFile 不会自动推断格式,扩展名不是 .yaml 时务必补 SetConfigType,否则报 Unsupported Config Type。
第三步:开启环境变量覆盖
v.AutomaticEnv()
v.SetEnvPrefix("APP") // APP_SERVER_PORT
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // server.port -> SERVER_PORT
优先级从高到低是:v.Set() > 命令行 flag > 环境变量 > 配置文件 > SetDefault()。所以设置 APP_SERVER_PORT=9090 后,v.GetInt("server.port") 返回 9090,配置文件里的 8080 被覆盖。生产环境正是靠这个把密码、密钥放到容器环境变量里,不进代码库。
注意:AutomaticEnv 只对“已经存在的键”生效。如果某个键在配置文件里压根没写,Get 会返回环境变量值,但 Unmarshal 到结构体时取不到——因为 Unmarshal 走的是 AllSettings,它只包含已知键。解决办法是给每个键 v.SetDefault("xxx", ""),或者显式 v.BindEnv("database.password", "DB_PASSWORD")。
第四步:解到结构体用
type Config struct {
Server struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
} `mapstructure:"server"`
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil { ... }
tag 必须是 mapstructure,写 json 不生效。Viper 默认开启弱类型转换,环境变量进来的字符串 "9090" 能正确落到 int 字段。
第五步:热加载
v.OnConfigChange(func(e fsnotify.Event) {
log.Println("配置变了:", e.Name)
var newCfg Config
if err := v.Unmarshal(&newCfg); err != nil {
log.Println("解析失败,保留旧配置:", err)
return
}
atomic.StorePointer(&cfgPtr, unsafe.Pointer(&newCfg)) // 或用互斥锁
})
v.WatchConfig()
WatchConfig 内部基于 fsnotify,配置变化时它会先 ReadInConfig 再回调,所以回调里直接 Unmarshal 就能拿到新值。
注意三个坑:一是热加载不会自动更新你之前已经 Unmarshal 好的那个结构体变量,必须自己重新解析并替换;二是新配置解析失败时,Viper 里的值可能已经被部分覆盖,最好先校验再启用;三是用 vim、VSCode 保存时文件可能被“删除重建”,inotify 监听会失效,此时需要重新 WatchConfig,或者把容器里的配置改成挂载单个文件以外的目录方式,测试时也别只看日志一次就以为好了。
第六步:并发读取要保护
回调在独立 goroutine 里跑,业务代码同时在读配置,直接改结构体会产生数据竞争。用 sync.RWMutex 包一层,或者像上面那样用 atomic.Pointer 整体替换,go build -race 跑一遍确认干净。
小结
SetConfigFile + SetConfigType 明确指定文件,避免多格式歧义。
- 环境变量三件套:
AutomaticEnv + SetEnvPrefix + SetEnvKeyReplacer,习惯用 APP_ 前缀防冲突。
Unmarshal 依赖已有键,缺键先 SetDefault 或 BindEnv。
- 热加载 =
OnConfigChange + WatchConfig,回调里重新 Unmarshal 并原子替换,解析失败就保留旧值。
- 结构体 tag 写
mapstructure,读写共享配置加锁。