fsnotify是唯一实用选择,因其跨平台封装inotify/kqueue/ReadDirectoryChangesW,低延迟、不漏事件;轮询os.Stat则耗CPU、易丢事件、无实时性且无法区分变更类型。

fsnotify 是唯一实用选择,轮询(os.Stat)在生产环境不可用。 它跨平台封装 inotify/kqueue/ReadDirectoryChangesW,不依赖定时检查,事件延迟低、不漏发,但需手动处理递归、符号链接和事件位掩码判断。
为什么不能用 os.Stat 轮询检测文件变化
常见错误是每秒调用一次 os.Stat 比对 ModTime() 或大小。这会导致:
- CPU 白耗——尤其在空闲时仍高频 syscall
- 事件丢失——编辑器保存、日志轮转等瞬时操作可能落在两次轮询之间
- 延迟不可控——最小响应时间就是轮询间隔,无法做到“实时”
- 无事件语义——你只能知道“变了”,但不知道是
Create、Write还是Rename
监听子目录必须手动递归添加
fsnotify.Watcher 默认不递归,w.Add("config/") 只监听该目录自身(如被 mv 或 chmod),不会收到 config/db.yml 的修改事件。
正确做法是遍历目录树,对每个子目录调用 w.Add():
立即学习“go语言免费学习笔记(深入)”;
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return watcher.Add(path)
}
return nil
})
- 用
filepath.WalkDir而非filepath.Walk,前者不跟随符号链接,更安全 - 新增子目录时,仅靠监听到
fsnotify.Create事件不够——你得在事件回调里主动watcher.Add(新路径) - macOS 深层嵌套(>100 层)可能触发
fsnotify.ErrEventOverflow,需捕获并降级处理
如何正确判断事件类型:别用 ==,要用 .Has()
fsnotify.Event.Op 是位掩码,不是单值枚举。比如文件被写入且权限变更,e.Op 可能是 fsnotify.Write | fsnotify.Chmod。
错误写法:if e.Op == fsnotify.Write —— 会漏掉组合事件。
正确写法:
if e.Op.Has(fsnotify.Write) {
// 处理写入(含 Write + Chmod、Write + Rename 等)
}
if e.Op.Has(fsnotify.Create) {
// 新建文件或目录
}
if e.Op.Has(fsnotify.Remove) {
// 删除(注意:也可能是移动出监听范围)
}
-
.Has()是唯一可靠的判断方式,.String()仅用于调试输出 -
fsnotify.Rename在 macOS 和 Windows 上可能拆分为两个事件(Remove+Create),不能假设原子性 - 监听路径必须存在,
watcher.Add()对不存在的路径会返回错误,需提前用os.Stat检查
最易被忽略的是资源清理:不调用 watcher.Close() 会导致文件描述符泄露,尤其在长期运行的服务中;而没启动 goroutine 读取 watcher.Events 和 watcher.Errors 通道,则事件会阻塞甚至永久丢失。


















