fsnotify 是 go 文件监控的唯一合理选择,因其封装了各平台原生 api、稳定跨平台且无需 c 依赖;正确使用需手动添加存在路径、遍历子目录、过滤临时文件、合并重复事件,并安全关闭以避免 goroutine 泄漏。

为什么 fsnotify 是 Golang 文件监控的唯一合理选择
标准库不提供文件系统事件监听能力,所有稳定、跨平台的方案都依赖 fsnotify —— 它是对 Linux inotify、macOS FSEvents、Windows ReadDirectoryChangesW 的封装。自己调用 syscall 或轮询(os.Stat)会丢事件、耗 CPU、不响应重命名或软链接变动。
直接使用 github.com/fsnotify/fsnotify 即可,无需额外 C 依赖或构建标签。
如何正确启动并过滤 fsnotify.Watcher
Watch 实例创建后必须主动添加路径,且路径需存在、有读权限;子目录不会自动递归监听,需手动遍历添加。常见错误是监听了符号链接本身却没处理其目标变化,或监听了不存在的路径导致 watch.Add 返回 error 被忽略。
- 用
filepath.WalkDir遍历目录树,对每个os.DirEntry调用watch.Add(注意跳过 symlink,除非你明确想监控链接文件自身) - 监听前先
os.Stat检查路径是否存在,避免静默失败 - 对
fsnotify.Event的Name字段做filepath.Base或filepath.Ext判断,而非直接字符串匹配,避免路径分隔符差异问题 - 事件类型如
fsnotify.Write、fsnotify.Create可能重复触发(尤其编辑器保存时),需结合去重逻辑(例如 100ms 内相同Name的Write合并)
fsnotify 在 macOS 上的特殊行为与绕过方式
macOS 的 FSEvents 不报告具体事件类型(如“文件被修改”还是“权限变更”),fsnotify 统一返回 fsnotify.Chmod 或空事件;同时对临时文件(如 .swp、~ 结尾)和编辑器原子写入(先写 tmp-xxx 再 rename)响应延迟或丢失 Create + Write 连续事件。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖
Event.Op判断内容是否变更,改用os.Stat比较ModTime()和Size() - 监听目录时,对
Event.Name做后缀过滤:忽略.*.swp、.*~、.DS_Store - 检测到
Rename事件后,立即对新旧路径都做os.Stat,确认是否为编辑器的原子替换(旧路径消失 + 新路径出现且大小非零)
如何安全关闭 Watcher 并避免 goroutine 泄漏
watch.Close() 不会自动清空已排队但未读取的事件,若在 watch.Events channel 上阻塞读取,Close() 后仍可能收到残留事件;同时,watch.Errors channel 若无人读取会阻塞写入,导致整个 watcher 卡死。
- 用
select+default非阻塞读取watch.Events和watch.Errors,或启动独立 goroutine 处理二者 - 关闭前先关闭自己的事件消费逻辑(例如通过
context.WithCancel控制主循环),再调用watch.Close() - 关闭后仍要读完
Events和Errorschannel 中剩余项(最多各 1 个),否则可能 panic:“send on closed channel” - 测试时用
time.AfterFunc强制超时关闭,防止因路径权限突变等意外卡住
真正的难点不在监听启动,而在事件语义的准确还原——比如区分“文件被 vim 编辑保存”和“被 cp 覆盖”,这需要结合 Op、ModTime、临时文件模式、甚至进程信息(需额外权限)交叉判断。别指望一个 Event 就能告诉你用户做了什么。










