fsnotify默认不递归监听,只响应被显式add的目录事件;新建子目录需在收到create事件后经os.stat确认为目录再调用watcher.add()动态添加,且路径不可带尾斜杠。

为什么 fsnotify 监听目录时会漏掉新建子目录里的文件
默认用 fsnotify.Watcher 监听一个目录,只监听该目录层级的事件,不会递归监听新创建的子目录。比如你监听 /data/inbox,当它下面新建了 /data/inbox/2024-06-15,这个子目录里的文件改动不会触发回调。
解决办法是手动在 fsnotify.Event.Op == fsnotify.Create 且 info.IsDir() 为 true 时,调用 watcher.Add() 加入新目录:
if event.Op&fsnotify.Create == fsnotify.Create {
info, err := os.Stat(event.Name)
if err == nil && info.IsDir() {
watcher.Add(event.Name) // 注意:不是 Add(event.Name + "/")
}
}
- 必须先
os.Stat确认是目录,避免对普通文件重复Add导致invalid argument错误 -
watcher.Add()传入路径不能带尾部斜杠,否则某些系统(如 macOS)会报错 - 递归添加后,也要注意避免监听临时文件或编辑器备份(如
*~、.swp),建议在事件过滤阶段就跳过
归档逻辑里怎么安全地移动文件而不中断写入
很多程序(如日志生成器、上传服务)会先创建空文件,再持续追加内容。如果监听到 Create 就立刻 os.Rename,大概率遇到 text file busy 或读取不完整。
正确做法是等文件“稳定”后再处理——即连续两次检查发现大小不变,且至少间隔 1–2 秒:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func isStable(path string) bool {
var lastSize int64 = -1
for i := 0; i
- 不要依赖
fsnotify.Write事件次数判断——有些程序一次性写完,只发一次Write;有些分块写,发多次 - 归档目标路径需确保父目录存在,用
os.MkdirAll(destDir, 0755),别假设上级目录一定存在 - 移动失败时,建议记录错误但不要 panic,否则整个监听可能中断;可重试 1 次,超时则放弃并打日志
如何避免同一文件被重复归档
文件系统事件可能重复(尤其 NFS 或某些容器环境),fsnotify 可能对同一个文件发出多个 Create 事件。直接按文件名归档会导致重复搬移甚至覆盖。
推荐用文件的 inode + device ID 做唯一标识(Linux/macOS 安全,Windows 需改用 os.FileInfo.Sys().(*syscall.Win32FileAttributeData) 的 FileIndexLow/High):
dev, ino := getInodeAndDevice(fi)
key := fmt.Sprintf("%d-%d", dev, ino)
- 内存中用
map[string]time.Time缓存最近 5 分钟内已处理过的 key,防止短时间重复 - 不要用文件名或修改时间做判重——重名文件、时钟回拨都会出问题
- 如果脚本重启,内存缓存丢失,可通过归档后的文件名规则(如加入时间戳哈希)规避跨重启重复,但通常没必要
运行时如何控制并发和资源泄漏
高频写入场景下,大量文件同时到达,若每个都起 goroutine 归档,容易堆积协程或打开过多文件句柄。
- 用带缓冲的 channel 控制并发数,例如
sem := make(chan struct{}, 5),每次归档前sem ,完成后 <code> -
fsnotify.Watcher是非线程安全的,所有Add/Remove必须在同一个 goroutine 中操作,建议用单独 goroutine 处理事件,并通过 channel 同步调用Add - 程序退出前务必调用
watcher.Close(),否则文件描述符泄漏,Linux 下最多 1024 个 watcher 就会失败 - 长时间运行时,定期清理已删除目录的监听(比如某子目录被手动删了,但还在 watcher 列表里),可用
os.IsNotExist(err)检查Stat失败后调用watcher.Remove()
实际部署时,最常被忽略的是子目录动态监听的异常分支——比如权限不足导致 Add 失败,但没记录日志,结果新目录下的文件彻底静默丢失。得在每个 watcher.Add() 后加 error check 并 warn。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










