直接用fsnotify.add("config.yaml")监听单个文件在90%编辑器保存场景下失效,因原子写入导致inode失效;必须监听父目录并用filepath.base(event.name)过滤,同时处理write和rename事件且优先响应后者。

直接用 fsnotify.Add("config.yaml") 监听单个文件,90% 场景下根本不会触发——编辑器保存时基本都走原子写入(先写临时文件再 rename),原文件 inode 已失效,监听自然失效。
必须监听父目录,再按文件名过滤
编辑器(VS Code、vim、Sublime)保存配置文件时,实际行为是:write config.yaml.tmp → rename config.yaml.tmp config.yaml。你监听的 "config.yaml" 在 rename 瞬间已被替换,旧监听句柄作废。
- 正确做法:用
watcher.Add("./configs")监听整个目录,收到事件后用filepath.Base(event.Name)提取文件名,再比对是否为"config.yaml" - 路径必须存在且可读:监听前先
os.Stat("./configs"),检查err == nil && info.IsDir() - 若目录含符号链接,用
filepath.EvalSymlinks("./configs")解析真实路径再监听,否则可能监听到链接本身
Write 和 Rename 事件都要注册,且 Rename 优先响应
fsnotify 的 event.Op 是位掩码,不是枚举值。只写 if e.Op == fsnotify.Write 会漏掉绝大多数真实变更。
- 注册监听时显式启用:
watcher.Add("./configs")后,fsnotify默认只接收部分事件,必须靠消费逻辑判断类型 - 判断 Rename:
if e.Op&fsnotify.Rename != 0 && filepath.Base(e.Name) == "config.yaml" - 判断 Write(追加类操作):
if e.Op&fsnotify.Write != 0 && filepath.Base(e.Name) == "config.yaml" - 收到事件后立刻
os.Stat(e.Name),确认文件存在、大小 > 0,跳过.swp、~、.tmp等临时后缀
执行命令前要确认文件真正写完
Linux 下 Write 事件可能在大文件写入中途就发出;macOS 可能因 Chmod 事件误判;Windows 对重命名延迟更敏感。
- 不推荐简单
time.Sleep(100 * time.Millisecond):不可靠,尤其在高 I/O 负载下 - 稳妥做法:收到
Rename事件即认为“已写完”,因为rename是原子操作 - 若只能依赖
Write,应两次读os.Stat(e.Name).ModTime(),间隔 50ms,确认时间戳不再变动 - 避免
exec.Command("sh", "-c", cmdStr)拼接字符串,改用exec.Command("sh", "-c", "your_cmd", "--", arg1, arg2)防注入和空格截断
递归监听子目录需手动遍历,别信“自动支持”
fsnotify 不提供 recursive: true 参数。监听 "./configs" 只覆盖该目录自身元数据(比如被重命名),对其下 ./configs/db/config.yaml 的修改完全无感。
- 用
filepath.WalkDir("./configs", ...)遍历所有子目录,对每个os.DirEntry调用watcher.Add(entry.Name()) - 跳过符号链接:
if entry.Type() & os.ModeSymlink != 0则 continue - Linux 上注意系统限制:
/proc/sys/fs/inotify/max_user_watches,监听大量子目录可能报too many open files - Windows 需启用长路径支持,否则嵌套深的路径(>260 字符)会静默失败
最易被忽略的是:Events 和 Errors channel 必须持续消费。它们是无缓冲 channel,一旦阻塞,整个 watcher 就卡死,后续任何事件都收不到——这不是 bug,是设计使然。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











