必须监听目录而非单个文件才能稳定捕获Create事件,因编辑器原子写入、临时文件覆盖及NFS/容器限制会使路径失效;正确做法是Add父目录、过滤event.Name和Op,加防抖并递归监听子目录。

直接监听单个文件几乎必然漏掉 Create 事件——编辑器原子写入、临时文件覆盖、NFS/容器环境限制都会让路径失效。必须监听目录,再在事件流中过滤。
为什么 watcher.Add("config.yaml") 收不到 Create
因为 fsnotify 监听的是路径的 inode 句柄,不是文件名。编辑器(如 VS Code、vim)保存时默认先写 config.yaml~ 或 .config.yaml.tmp,再 rename 覆盖原路径。此时原 inode 已销毁,Create 实际发生在新文件上,但你没监听那个路径。
-
watcher.Add("config.yaml")只对这个确切路径生效,一旦被 rename 或 unlink,监听即失效 - 临时文件名不固定,无法提前
Add;且Create事件可能比Rename先到,顺序不可靠 - 容器或 NFS 挂载点下,inotify 本身就不工作,
Add可能静默失败(错误是no space left on device,实为ENOSPC)
正确监听目录 + 过滤 Create 的写法
监听父目录,用 filepath.Base(event.Name) 和 event.Op & fsnotify.Create 组合判断,才能稳定捕获新建动作。
- 调用
watcher.Add("configs/")(末尾带斜杠更安全),不是"configs/config.yaml" - 在事件处理 goroutine 中,用位运算判断:
if event.Op&fsnotify.Create != 0,不是== fsnotify.Create - 用
filepath.Base(event.Name)提取文件名,再匹配"config.yaml",避免误触config.yaml.bak或config.yaml~ - 加一个简单防抖:同一
event.Name在 100ms 内只处理一次,防止编辑器连发多个Create(如 Windows 下常见)
递归监听子目录里的 Create 事件
fsnotify 默认不递归,w.Add("configs/") 不会自动监听 configs/db/ 下的新文件。必须手动遍历并注册。
立即学习“go语言免费学习笔记(深入)”;
- 用
filepath.WalkDir遍历所有子目录,对每个dirEntry.IsDir()为true的路径调用watcher.Add(path) - 收到
fsnotify.Create且event.Name是目录时,立即watcher.Add(event.Name)—— 否则后续该目录下的新建文件收不到通知 - 注意符号链接:默认跳过,若需跟随,改用
filepath.Walk并检查os.ModeSymlink - macOS 上嵌套过深(>100 层)可能触发
kqueue溢出,需捕获fsnotify.ErrEventOverflow
容易被忽略的跨平台细节
Windows 和 macOS 对 Create 的语义理解不同,不统一处理会导致本地测试通过、上线后失效。
- Windows 下
fsnotify.Create可能触发两次:一次空文件创建,一次写入内容;建议用os.Stat(event.Name)确认文件大小 > 0 再处理 - macOS 的
kqueue对Remove事件支持弱,删除后常收不到;可改为监听父目录 + 定期os.Stat确认文件是否消失 - 所有路径比较前必须用
filepath.Clean(event.Name),Windows 返回反斜杠\,别用strings.Contains(event.Name, "/") - 务必在
defer watcher.Close()前启动事件消费 goroutine,否则watcher.Events缓冲区满会阻塞甚至 panic
最隐蔽的坑是:你以为监听了目录就万事大吉,但没处理 fsnotify.Rename 和 fsnotify.Chmod,而编辑器保存时根本不会触发 Write —— 它只改权限再重命名。真正可靠的判断是:只要 event.Name 匹配目标文件名,且 event.Op 中任意一位被置位,就重新加载。


















