fsnotify是go生态中唯一稳定、跨平台的文件变更监听方案,封装linux inotify、macos fsevents和windows readdirectorychangesw,行为一致;必须监听目录而非文件,需手动递归注册子目录,统一响应write/create/rename事件并加去重与延迟读取,且必须显式close防止fd泄漏。

fsnotify 是唯一能跨平台实时监听文件变更的方案
别折腾轮询或自己封装 syscall —— fsnotify 就是 Go 生态里唯一稳定、可维护、跨平台的解法。它不是“推荐库”,而是事实标准:Linux 用 inotify,macOS 用 FSEvents,Windows 用 ReadDirectoryChangesW,fsnotify 全部封装好了,行为一致,不用你操心系统差异。
常见错误是写个 time.Ticker 每秒调一次 os.Stat() 或算文件哈希。这会漏掉编辑器原子保存(比如 VS Code 写临时文件再 rename)、CPU 白耗、在高 I/O 场景下延迟飙升,还根本没法响应瞬间变更。
必须注意:fsnotify.Watcher 监听的是目录,不是文件。想监控 config.yaml,得 w.Add("config/"),然后在事件中用 event.Name == "config.yaml" 过滤。
如何正确监听子目录(递归监听)
fsnotify 默认不递归,这不是 bug,是设计。调用 w.Add("dir/") 只监听该目录自身事件(比如被 mv 或 chmod),不会收到 dir/a/b/c.txt 的 Write 事件。
- 用
filepath.WalkDir遍历所有子目录,对每个info.IsDir()为true的路径调用w.Add(path) - 符号链接默认跳过;如需跟随,改用
filepath.Walk并手动判断info.Mode() & os.ModeSymlink - 新增子目录时,仅靠监听
fsnotify.Create不够——你得在事件回调里主动w.Add(newDir),否则后续改动收不到 - macOS 上嵌套层级超过 100 层可能触发
kqueue限制,需捕获fsnotify.ErrEventOverflow并降级处理
Write / Create / Rename 事件怎么统一处理才不丢变更
不同编辑器保存策略完全不同:VS Code 先 Create 临时文件,再 Rename 覆盖原文件;Vim 直接 Write 原文件;Sublime Text 可能先 Remove 再 Create。只监听一种事件必然漏。
稳妥做法是:对所有写相关事件都响应,并加两层防护:
- 用
map[string]time.Time缓存最近触发的路径与时间,100ms 内相同路径重复事件直接丢弃 - 收到事件后启动
time.AfterFunc(100 * time.Millisecond, func()),等写操作真正落盘再读取内容 - 绝对不要在事件回调里直接解析 YAML/JSON 或做 IO —— 出错会卡死整个监听 goroutine;应发到 channel,交给独立 worker 处理
Watcher 生命周期和资源泄漏必须手动管
fsnotify.Watcher 不是线程安全的,Add/Remove 不能并发调用;更关键的是,它必须显式 Close(),否则 fd 泄露 —— Linux 默认最多 8192 个 inotify 实例,超限后新监听直接失败,报错 too many open files。
常见陷阱:
-
defer watcher.Close()只适用于短生命周期程序;长期运行服务必须支持动态增删路径 - 推荐封装成结构体,带
sync.RWMutex保护watcher和路径映射表,AddPath/RemovePath方法内部加锁 -
Errorschannel 会在监听失败(如权限不足)后发一条 error 然后关闭;若不select处理,goroutine 会永久阻塞泄露
最易被忽略的一点:监听路径必须真实存在。w.Add("missing/dir") 会直接返回 no such file or directory 错误,fsnotify 不会帮你创建父目录。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











