用air配热重启、viper.WatchConfig()配配置热更新,二者职责分明;air管代码变更是否重建,viper管配置文件变更是否重读生效;漏掉ReadInConfig()或Unmarshal()、路径不匹配、SetConfigType顺序错误,均导致静默失效。

直接上结论:用 air 配热重启、用 viper.WatchConfig() 配配置热更新,二者不互斥但职责分明——air 管代码改了要不要重新编译运行,viper 管 config.yaml 改了要不要重读生效。配错一个路径或漏掉一次 ReadInConfig(),就静默失效。
air 启动没反应?先盯住这三个路径
90% 的“改了不重建”问题,根源是路径没对齐:
-
root必须指向含go.mod的目录(通常是项目根),不是main.go所在目录;设成绝对路径易跨机器失效,留空或填.更稳 -
build.bin和build.cmd输出路径必须完全一致,比如都设为./tmp/main;不一致会报exec: "./tmp/main": file does not exist -
[watch]和[build]两节的include_ext必须相同,例如都加["go", "yaml"];只在 watch 里加yaml,build 不认,改配置文件就不会触发重建
viper.WatchConfig() 改了文件却没生效?回调里缺关键两步
viper.WatchConfig() 只发通知,不读文件、不解析、不更新任何变量。常见现象是日志打印“配置已更新”,但 viper.GetInt("port") 还是旧值。
- 回调第一行必须是
if err := viper.ReadInConfig(); err != nil { return },否则内部缓存仍是旧内容 - 紧接着必须
if err := viper.Unmarshal(&newCfg); err != nil { return },Unmarshal()才把新解析结果写进 Go 结构体;漏掉这步,cfg.Port永远卡在初始化那一刻 -
viper.SetConfigType("yaml")必须在ReadInConfig()之前调用,否则类型不匹配会静默失败(比如 YAML 被当 JSON 解析)
并发读配置 panic?别直接赋值,用 atomic.Value 存指针
直接写 cfg = newCfg 或逐字段赋值(如 cfg.Port = 8080)是非原子操作。多个 goroutine 同时读时,可能一个读到新 Port、另一个还读着旧 DBURL,尤其结构体含指针或嵌套 map 时极易 panic。
立即学习“go语言免费学习笔记(深入)”;
- 定义
var globalConf atomic.Value,初始化时globalConf.Store(&defaultCfg) - 热更新成功后,必须
globalConf.Store(&newCfg)(注意取地址,不是值) - 业务代码统一用
conf := globalConf.Load().(*Config)读取,类型断言不能省,且Load()后建议判空
Docker/K8s 里 WatchConfig 静默失效?不是磁盘满,是 inotify 句柄耗尽
现象是“改了文件没反应”,控制台无报错。Linux 容器默认 fs.inotify.max_user_watches 极低(常为 8192),报错 No space left on device 不是磁盘满,而是 inotify 句柄用尽。
- Docker 启动必须加参数:
--sysctl fs.inotify.max_user_watches=524288,只调高宿主机无效,容器内也要一致 - K8s 中 ConfigMap 挂载后不更新?先检查挂载点权限:
ls -l /etc/config/app.yaml,确保可读 - Windows 下路径一律用正斜杠
/,哪怕在 PowerShell 里写.\tmp\main也会导致监听失效
真正卡住人的地方,从来不是“会不会配”,而是改完一行配置后,不确定它到底有没有被 air 看见、有没有被 viper 解析、有没有被业务 goroutine 读到——每层都得有可观测落点,比如日志打上文件 ModTime().Unix()、启动时加信号等待配置就绪,而不是靠 sleep 硬等。


















