fsnotify是Go监听文件变化最可靠方案,因其封装inotify/FSEvents/ReadDirectoryChangesW实现跨平台事件驱动,避免轮询缺陷;需监听目录、处理原子写入、加锁热加载并安全关闭。

用 fsnotify 监听文件变化最可靠
Go 标准库不提供跨平台的文件系统事件监听,fsnotify 是事实标准。它封装了 Linux 的 inotify、macOS 的 FSEvents 和 Windows 的 ReadDirectoryChangesW,避免自己写平台适配逻辑。
常见错误是直接轮询 os.Stat,这既耗 CPU 又有延迟(至少 100ms),且无法区分“修改完成”和“正在写入中”。fsnotify 能精确捕获 fsnotify.Write 事件,但要注意:编辑器保存时常先写临时文件再原子替换,此时触发的是 fsnotify.Create + fsnotify.Remove,不是 Write。
- 安装:
go get github.com/fsnotify/fsnotify - 监听路径必须是**目录**,不能是单个文件(否则 macOS 下可能漏事件)
- 注册后需单独启动 goroutine 读取
watcher.Eventschannel,否则会阻塞 - 收到
Write或Create后,建议加 10–100ms 延迟再 reload,防止读到半截内容
热加载配置文件要处理解析失败和原子性
配置热加载不是简单地重新 json.Unmarshal,核心矛盾在于:新配置加载失败时,不能让服务退回到无效状态;同时旧配置被替换的瞬间,其他 goroutine 正在读取,不能 panic 或读到中间态。
典型做法是用指针+互斥锁双保险:
var (
mu sync.RWMutex
cfg *Config
)
func reloadConfig() error {
newCfg := &Config{}
if err := json.Unmarshal(data, newCfg); err != nil {
return err // 不覆盖旧 cfg
}
mu.Lock()
cfg = newCfg
mu.Unlock()
return nil
}
func GetConfig() *Config {
mu.RLock()
defer mu.RUnlock()
return cfg
}
- 永远不直接赋值
cfg = ...,必须加锁 - 解析失败时旧配置保持可用,服务不中断
- 如果配置含切片或 map,确保它们是深拷贝或不可变结构,否则并发读写仍可能 panic
热加载代码逻辑需避免 goroutine 泄漏
很多 demo 直接在 for 循环里起新 goroutine 执行 reload,但没做取消控制,导致旧 reload 还在跑,新 reload 又来了——配置可能被反复覆盖,甚至并发执行初始化逻辑(比如重复连接数据库)。
- 用
sync.Once包裹一次性初始化动作(如 DB 连接池重建) - 对可取消操作(如 HTTP server 关闭),传入
context.Context并监听ctx.Done() - watcher 本身也要支持关闭:
watcher.Close(),并在程序退出前调用 - 不要在 reload 函数里启长期 goroutine(如
time.Tick),除非明确管理其生命周期
编辑器保存行为导致事件误判很常见
VS Code、vim 等默认启用“原子写入”,即先写 config.json~12345,再 rename 覆盖原文件。这时你监听到的是 Create + Remove,而不是 Write。如果只响应 Write,热加载就失效了。
- 必须同时监听
fsnotify.Create和fsnotify.Write事件 - 收到
Create后,检查文件名是否以~或.tmp结尾,跳过临时文件 - 更稳妥的做法:收到任意疑似变更事件后,用
os.SameFile对比当前文件和原始文件的dev/inode,确认是否为同一文件 - Windows 下某些编辑器用“覆盖写入”,不会触发
Create,所以不能只依赖Create
真正难的不是监听,而是判断“这次变更是否可信、是否已完成、是否该触发 reload”。多数线上事故都出在没处理好编辑器行为差异和并发安全边界上。


















