fsnotify.Add()监听不到子目录文件修改是因为其仅支持单层目录监听,不递归;需启动时用filepath.WalkDir全量注册子目录,运行时监听Create事件动态Add新目录,并加锁保护、跳过符号链接。

fsnotify.Add() 为什么监听不到子目录里的文件修改
因为 fsnotify.Add() 只注册单层目录监听,它不递归。调用 watcher.Add("config/") 后,你只会收到 config/ 目录自身的事件(比如被 mv 或 chmod),对 config/db.yml、config/nested/file.json 的任何改动都完全无感。
这不是 bug,是设计使然:底层 inotify/kqueue/ReadDirectoryChangesW 全都不支持递归注册,fsnotify 必须手动补足这一层。
- 启动时全量注册:用
filepath.WalkDir(root, ...)遍历所有子目录,对每个entry.IsDir()为true的路径调一次watcher.Add(path) - 运行时动态响应:在事件循环中监听
fsnotify.Create且event.IsDir()为true,立即watcher.Add(event.Name) - 加
sync.Mutex保护Add调用,避免多 goroutine 并发注册 panic - 跳过符号链接:
if entry.Type()&os.ModeSymlink != 0,否则可能触发权限错误或无限递归
如何安全过滤出目标文件的 Rename 和 Write 事件
编辑器(Vim、VS Code、TextEdit)保存文件时基本都走原子写入流程:新建临时文件 → 写入内容 → Rename 覆盖原文件。此时原 inode 已销毁,只监听 Write 会读到临时文件(如 .config.yaml12345)或直接漏事件。
必须同时监听 fsnotify.Write 和 fsnotify.Rename,且优先响应后者——Rename 才代表“保存完成”。
立即学习“go语言免费学习笔记(深入)”;
- 用位运算判断:
if event.Op&fsnotify.Rename != 0 && filepath.Base(event.Name) == "config.yaml",别用== fsnotify.Rename或strings.Contains(event.String(), "RENAME") - 收到事件后立刻
os.Stat(event.Name)确认文件存在、非空、不是临时后缀(如~、.swp、.tmp) - 某些编辑器(macOS TextEdit)还会夹带
fsnotify.Chmod,所以不要忽略组合操作位
watcher.Events channel 不带缓冲会丢事件
watcher.Events 是一个无缓冲 channel,操作系统事件产生速度远高于你的处理逻辑。一旦消费不及时,后续事件就会阻塞 watcher 内部 goroutine,轻则丢事件,重则死锁。
- 必须用带缓冲 channel 中转,例如:
events := make(chan fsnotify.Event, 100) - 另起 goroutine 拉取
watcher.Events并转发到该缓冲 channel - 永远检查
event.Op而非只看event.Name,因为同一事件可能含多个操作位(如fsnotify.Write | fsnotify.Chmod) - Windows 下
fsnotify.Create可能触发两次,需用文件大小或time.Sleep(10 * time.Millisecond)去抖动
监听路径含软链接时容易注册到错误位置
如果你直接 watcher.Add("config.yaml"),而该路径实际是符号链接,filepath.Dir() 会返回链接所在目录而非目标文件所在目录。结果就是监听注册到了软链接父目录,但目标文件变更不会触发事件。
- 务必先用
filepath.EvalSymlinks("config.yaml")解析真实路径,再用filepath.Dir()取父目录 - macOS 对符号链接默认不追踪目标文件变更,Windows 则会;跨平台行为差异大,不能依赖默认行为
- 监听路径必须存在,
fsnotify.Watcher不会自动创建父目录,也**不会报错提示**,只是静默失败 - Linux 下注意
/proc/sys/fs/inotify/max_user_watches太小会导致添加失败,错误可能是no space left on device,而非明显路径错误


















