fsnotify递归监听默认不生效,因其Watcher仅监听显式Add()的路径,子目录需手动遍历添加;启动时用filepath.WalkDir全量注册,运行时监听Create事件动态Add新目录,并加锁保护操作。

为什么 fsnotify 的递归监听默认不生效
因为 fsnotify.Watcher 本身不支持递归,它只监听你显式 Add() 的路径;子目录不会自动加入监听队列,哪怕你加了父目录。这是设计使然,不是 bug。
常见错误现象:fsnotify.Event.Name 只返回顶层目录下的文件变更,新创建的子目录里改文件完全没事件;或者程序启动后新增的子目录始终“静默”。
- 必须手动遍历目录树,对每个子目录调用
watcher.Add() - 监听期间新建的子目录,需额外监听父目录的
fsnotify.Create事件,再触发Add() - Windows 下注意:某些杀毒软件或 OneDrive 会拦截底层 inotify/ReadDirectoryChangesW,导致漏事件
如何安全地初始化递归监听(含新建子目录)
核心是两步:启动时全量扫描 + 运行时动态响应目录创建。别指望一次 Add() 能搞定所有层级。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
filepath.WalkDir()(Go 1.16+)遍历目录,对每个fs.FileInfo.IsDir() == true的路径调用watcher.Add() - 在事件循环中,对收到的
fsnotify.Create且event.IsDir()为 true 的事件,立刻watcher.Add(event.Name) - 加锁保护
watcher操作——Add()和事件循环可能并发,否则 panic:“invalid memory address”
示例片段:
// 初始化递归监听
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil { return err }
if d.IsDir() {
mu.Lock()
defer mu.Unlock()
_ = watcher.Add(path)
}
return nil
})
Event.Name 返回路径不一致?注意平台和监听路径写法
fsnotify.Event.Name 是相对监听路径的,不是绝对路径;不同系统行为略有差异,容易误判文件归属。
常见坑:
- 你在
/a/b调用watcher.Add("/a/b"),那么event.Name是类似/a/b/file.txt(Linux/macOS),但 Windows 可能带盘符、斜杠方向混乱 - 如果监听的是相对路径如
"./data",event.Name也会是相对路径,后续拼接路径易出错 - 跨平台安全做法:初始化时用
filepath.Abs(root)统一转成绝对路径,事件中用filepath.Join(root, ...)或直接比较前缀
性能与资源泄漏风险点
递归监听本质是 N 个独立监听句柄(inotify 实例 / kqueue fd / ReadDirectoryChangesW handle),数量失控会直接耗尽系统资源。
- Linux 默认
/proc/sys/fs/inotify/max_user_watches通常只有 8192,监听几千个目录就爆了——需提前调大或做目录粒度收敛 - 不要对临时目录(如
/tmp)、容器挂载点、网络文件系统(NFS/SMB)盲目递归,事件风暴或阻塞很常见 - 务必在退出前调用
watcher.Close(),否则 fd 泄漏;若用defer,注意它只在函数返回时触发,长生命周期服务要自己管理关闭时机
真正麻烦的从来不是怎么加监听,而是怎么判断哪些子目录值得监听、哪些该忽略、以及监听后如何避免重复事件和路径歧义。










