fsnotify 是唯一靠谱的跨平台文件监听方案,标准库不支持;必须监听目录而非文件,用位运算判断事件类型,手动递归添加子目录,并注意权限、符号链接、编辑器原子写入及平台差异。

直接用 fsnotify 是唯一靠谱的选择,标准库不提供跨平台文件监听能力,轮询(os.Stat 定时检查)延迟高、CPU 毛刺大、还漏事件——别试。
为什么 fsnotify.Add() 没反应?路径和权限是硬门槛
常见现象:调用 watcher.Add("/path/to/config.yaml") 后改文件,watcher.Events 通道一直空。根本原因不是代码逻辑错,而是路径本身不满足监听前提:
-
os.Stat("/path/to/config.yaml")必须成功返回,路径必须存在且有读权限;监听目录时,Linux/macOS 还需执行(x)权限才能遍历内容 - Windows 下若文件正被 Excel/Word 独占打开,
Rename或Remove事件大概率收不到 - 符号链接(symlink)默认只监听链接自身,不追踪目标变更;如需监听真实路径,先用
filepath.EvalSymlinks()解析再Add() - macOS 上对已删除目录发
Remove事件不可靠,建议监听父目录 +os.Stat()二次确认
如何监听整个目录树(含子目录)?fsnotify 不递归是设计使然
fsnotify 默认只监听指定路径一级,子目录不会自动加入。这不是 bug,是为避免误监大量无关路径导致 inotify 资源耗尽(Linux 上 /proc/sys/fs/inotify/max_user_watches 有上限)。正确做法是手动遍历并逐个添加:
- 用
filepath.WalkDir()遍历目录树,对每个os.DirEntry调用watcher.Add(entry.Name()) - 跳过
entry.Type().IsDir()为 false 的项(即文件),除非你明确要监听单个文件 - 遇到 symlink 时,用
entry.Type() & os.ModeSymlink != 0判断,按需决定是否Add()其目标 - 注意:添加顺序不影响事件分发,但失败的
Add()会返回 error,不能忽略
Event.Op 是位掩码,不是枚举值——if event.Op == fsnotify.Write 会失效
编辑器保存文件时,常同时触发写入和权限变更,event.Op 实际值可能是 fsnotify.Write | fsnotify.Chmod。用 == 判断必然漏事件:
立即学习“go语言免费学习笔记(深入)”;
- 正确写法是:
if event.Op&fsnotify.Write != 0—— 只要包含写操作就响应 -
Create和Write都可能对应“新文件”,区别在于:空文件先Create后Write,而cp或原子写入(vim/vscode)通常只发Create - macOS 的 FSEvents 不区分具体操作类型,
Op常为fsnotify.Chmod或 0;此时得 fallback 到os.Stat()比较ModTime()和Size() - 临时文件(
.swp、~结尾、.DS_Store)应过滤:用filepath.Base(event.Name)提取文件名再判断后缀
事件通道卡死、重复触发、goroutine 泄漏——这些才是线上真坑
新手常把 for range watcher.Events 写在主 goroutine,结果一卡全卡;或忘记关 watcher,跑几天后进程句柄爆满:
-
watcher.Events和watcher.Errors都是非缓冲 channel,必须另起 goroutine 消费,否则 watcher 内部会阻塞并丢后续事件 - 编辑器保存、IDE 索引、
git checkout常在 10ms 内连发多个Write,直接处理会导致逻辑错乱;必须加防抖:例如用time.AfterFunc延迟 100ms 执行,每次新事件到达就Stop()旧定时器 -
watcher.Close()前,先通过context.WithCancel退出事件循环,再关闭;关闭后仍需非阻塞读一次watcher.Events和watcher.Errors,否则可能 panic “send on closed channel” - Windows 对长路径(>260 字符)支持差,启用 long path 需系统策略开启,且 Go 项目中需加
//go:build windows构建约束
真正难的从来不是“怎么监听”,而是“怎么不漏、不卡、不崩、不误判”——尤其在 macOS 和 Windows 上,同一段代码行为差异极大,必须按平台做兜底校验。


















