Go-fsnotify已被弃用,应改用fsnotify;其默认不递归监听子目录,需手动遍历添加并处理CREATE事件;Windows下符号链接需先解析;热重载配置时须延迟去抖、过滤写事件、异步执行。

Go-fsnotify 已被官方弃用,当前应直接使用 fsnotify(即 github.com/fsnotify/fsnotify),它就是原 go-fsnotify 的继承者和唯一维护版本。强行找旧包或尝试兼容老文档,大概率会遇到构建失败、事件丢失或 panic。
为什么 fsnotify 不能直接监听整个目录树
fsnotify.Watcher 默认只监听单层目录,子目录变更不会自动递归触发。这不是 bug,是底层 inotify/kqueue 的设计限制 —— 每个子目录需单独 watcher.Add()。
- 若只
watcher.Add("config/"),则config/db.yaml变更能收到,但config/dev/cache.json不会触发 - 递归监听需手动遍历 + 逐个添加,且要处理新增子目录的后续监听(即监听
CREATE事件后对新目录再Add()) - Windows 上对符号链接目录监听可能静默失败,建议提前
filepath.EvalSymlinks()
如何安全地监听配置文件并热重载
典型场景:监听 ./conf/ 下所有 .yaml 和 .toml 文件,文件写入完成后再触发重载(避免读到半截内容)。
- 用
filepath.WalkDir()初始化时递归添加匹配文件,跳过目录本身 - 对
fsnotify.Write事件加 100ms 延迟 debounce(用time.AfterFunc或简单 channel 缓冲),防止编辑器多次写入触发多次重载 - 务必检查
event.Op&fsnotify.Write != 0,而非仅判断event.Op == fsnotify.Write—— 实际中常混着Chmod或Rename - 重载逻辑必须在 goroutine 中执行,否则阻塞事件循环导致后续事件积压
// 示例:监听 conf/ 下所有 .yaml
watcher, _ := fsnotify.NewWatcher()
defer watcher.Close()
filepath.WalkDir("conf/", func(path string, d fs.DirEntry, err error) {
if !d.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".toml")) {
watcher.Add(path)
}
})
go func() {
for {
select {
case event := <-watcher.Events:
if (event.Op&fsnotify.Write) != 0 {
time.AfterFunc(100*time.Millisecond, func() {
reloadConfig(event.Name) // 确保文件已写完
})
}
case err := <-watcher.Errors:
log.Println("watcher error:", err)
}
}
}()
常见 panic 和静默失效原因
多数线上问题不是“不触发”,而是触发后 panic 或漏掉事件,集中在三类:
立即学习“go语言免费学习笔记(深入)”;
-
panic: send on closed channel:watcher.Close()后仍往watcher.Events发送 —— 所有事件消费逻辑必须在watcher.Close()前退出,推荐用sync.WaitGroup等待 goroutine 结束 - Linux 上 inotify instance 耗尽:每个
Add()占一个 inotify watch,/proc/sys/fs/inotify/max_user_watches默认仅 8192,监控大量小文件时易超限;可通过echo 524288 | sudo tee /proc/sys/fs/inotify/max_user_watches临时调高 - macOS 上
fsnotify对mv操作可能只发Remove无Create:编辑器保存时常先写临时文件再 rename,应同时监听源目录和临时文件所在目录(如/var/folders/...)或改用Write+Chmod组合判断
递归监听、跨平台行为差异、资源泄漏这三点,比“怎么加监听”本身更值得花时间验证。别依赖本地测试结果就上线 —— macOS 和 Linux 对 rename 的处理逻辑不同,而生产环境几乎全是 Linux。


















