viper.WatchConfig() 本身不阻塞也不自动重读,需提前配置路径与文件名、注册OnConfigChange回调、监听具体绝对路径文件,并在回调中手动ReadInConfig、Unmarshal及原子替换配置。

viper.WatchConfig() 为什么改了文件没反应
它只注册监听器,不阻塞主线程,也不自动重读配置。调完 viper.WatchConfig() 后如果 main() 立即返回,整个进程就退出了,监听根本没机会触发。
-
viper.AddConfigPath()和viper.SetConfigName()没在viper.WatchConfig()前完成,导致回调里viper.ReadInConfig()报Config File Not Found - 没显式调用
viper.OnConfigChange()注册回调,事件被丢弃 - 监听路径是目录(如
"./configs"),但编辑器保存时先写临时文件再 rename,触发的是fsnotify.Create + fsnotify.Remove,不是Write;macOS 下尤其容易漏事件 - 容器中
inotify.max_user_watches耗尽(Docker 默认仅 8192),需启动时加--sysctl fs.inotify.max_user_watches=524288
OnConfigChange 回调里必须手动做三件事
漏掉任一环节,viper.Get("port") 和结构体字段都还是旧值——viper 不会自动刷新缓存,也不会写回你的变量。
- 第一行必须是
if err := viper.ReadInConfig(); err != nil { log.Printf("reload failed: %v", err); return }:否则内部map[string]interface{}缓存仍是旧内容 - 紧接着要
if err := viper.Unmarshal(&newCfg); err != nil { /* 跳过替换 */ }:不调这个,你的cfg.DBURL永远卡在初始化那一刻 - 必须用
atomic.Value或sync.RWMutex原子替换指针,不能直接赋值cfg = newCfg——否则 goroutine 可能读到 Port 已更新但 DBURL 还是旧的“半中间态”
监听路径必须是具体文件的绝对路径
别信“监听目录更省事”。viper.WatchConfig() 的行为依赖底层 fsnotify,而后者对路径精度极其敏感。
- 正确做法:
viper.SetConfigFile("/app/config.yaml"),再调viper.WatchConfig();viper 会基于该路径自动监听 - 错误做法:只设
viper.AddConfigPath("./configs")+viper.SetConfigName("config"),期望它自动推断并监听./configs/config.yaml——它不会 - 开发调试时加日志:
viper.OnConfigChange(func(e fsnotify.Event) { log.Printf("event: %+v", e) }),确认是否真收到事件;收到后还要检查e.Name == "config.yaml",过滤掉.swp、.tmp等干扰
signal.Notify 收到 SIGHUP 后不能只打印日志
os.Signal 只负责接收信号,不处理进程生命周期。收到 SIGHUP 后若没走完整热加载路径,配置依然不会更新。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
signal.Notify(sigChan, syscall.SIGHUP)捕获,且sigChan是带缓冲的 channel(至少 cap=1),否则信号可能丢失 - -exec 命令别写成
sh -c "pkill && ./myapp",shell 会吞掉信号;应直接写./myapp或用exec.Command - 收到
SIGHUP后,不要 reloadconfig.yaml再覆盖全局变量——要走和文件监听一致的路径:校验 → 新建实例 → 原子替换 - 本地调试时关掉 consul-template 的健康检查(加
-disable-health-checks),否则改一次配置要等 30 秒才触发
配置热加载真正难的不是监听或解析,而是原子性保证与失败兜底:解析失败时不能 panic,也不能让新旧配置混杂;并发读写时不能出现 data race;信号或文件事件来了,得确保只执行一次完整流程,而不是重复触发、部分生效。这些点稍不注意,就会在线上表现为偶发性配置错乱,极难复现。


















