fsnotify监听必须监听父目录而非单个文件,因编辑器atomic write导致inode失效;需用filepath.base过滤、os.stat校验、atomic.value原子更新配置,并在回调中手动调用readinconfig和unmarshal。

fsnotify监听必须监听目录而非单个文件
直接watcher.Add("config.yaml")在真实编辑器(VS Code、vim)下大概率失效,因为它们用 atomic write:先写config.yaml~或.config.yaml.swp,再rename覆盖原文件。原 inode 已失效,事件丢失。
正确做法是监听父目录,再靠文件名过滤:
-
watcher.Add("./conf")(不是"./conf/config.yaml") - 收到事件后,用
filepath.Base(event.Name) == "config.yaml"精确匹配 - 只响应
event.Op & fsnotify.Write != 0或event.Op & fsnotify.Create != 0,忽略Chmod(编辑器常触发但不意味内容变更) - 事件到达后立刻
os.Stat(event.Name),确认文件存在且Size() > 0,跳过临时文件
viper.WatchConfig()只是发通知,不自动刷新值
调用viper.WatchConfig()后,日志显示“config changed”,但viper.GetString("db.addr")仍返回旧值——它只往 channel 发事件,不重读文件也不更新内部缓存。
必须在回调里手动完成完整流程:
- 回调中调用
viper.ReadInConfig()重新加载磁盘内容 - 再用
viper.Unmarshal(&newCfg)生成全新结构体实例(不能复用旧指针) - 校验
newCfg字段合法性(如端口范围、URL 格式),失败则跳过替换 - 通过
atomic.Value.Store()原子替换配置指针,而非赋值全局变量
配置读取必须每次走原子 Load,禁止缓存指针
业务代码里写cfg := globalConfig.Load().(*Config)没问题,但若接着缓存dbAddr := cfg.DB.Addr,下次热加载后这个dbAddr仍是旧值——它指向的是上一轮的内存地址。
所有读取都得实时穿透:
- HTTP handler 中不要提前把
viper.GetString("http.port")存为包级变量 - 数据库连接池大小变更后,必须显式调
db.SetMaxOpenConns(cfg.PoolSize),改配置字段本身无效 - 日志级别更新需调
logger.Level().SetLevel(zapcore.Level(cfg.LogLevel)),不是改cfg.LogLevel就完事 -
atomic.Value.Load()返回的是新实例指针,每次调用都拿到当前最新快照
解析失败必须保留旧配置并记录带行号错误
YAML 缩进错、JSON 字段类型不匹配、必填字段漏写……这些日常操作会让yaml.Unmarshal()返回 error。此时若 panic 或静默丢弃,服务可能瞬间退化。
安全兜底逻辑要硬编码:
- 错误日志必须包含具体位置:
config.yaml: line 12: cannot unmarshal !!str into int(用yaml.WithStrict()+ 自定义Decoder可获取行号) - 暴露
/debug/config/status接口,返回上次成功时间、最近错误详情、当前配置哈希 - 避免在回调里做耗时操作(如拉远程配置、重连 DB),应发 signal 或 channel 到主 goroutine 处理
- Linux 下注意
inotify句柄数限制,watcher.Add()太多路径会报too many open files,需调高ulimit -n
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











