必须监听父目录并在事件回调中用filepath.base()提取文件名、filepath.ext()判断扩展名,统一转小写比较,同时处理write/create/rename事件并校验文件可读且非空。

如何用 fsnotify 监控目录并按扩展名过滤文件
直接用 fsnotify 监控目录时,它不区分文件类型,所有事件(包括临时文件、隐藏文件、非目标扩展名)都会触发。必须在回调里做二次过滤,不能依赖监听路径本身限制扩展名。
常见错误是把扩展名判断写在 event.Op 判断之后但没处理重命名场景:比如 .tmp 文件被重命名为 .pdf,此时 event.Name 是新名字,但监听逻辑若只看创建事件就可能漏掉。正确做法是:对 Write、Create、Rename 三类操作都提取最终文件名,再用 filepath.Ext() 判断。
- 用
filepath.Base(event.Name)获取文件名,避免路径干扰 - 统一转小写比较扩展名(Windows 不区分大小写,Linux 可能有大写后缀)
- 跳过以
.开头的文件(如.DS_Store、.swp),防止编辑器临时文件误触 - 注意
Rename事件会触发两次(旧名删除 + 新名创建),优先以event.RenameTo(如果存在)为准
扫描前校验文件是否真正可读且非空
监听到文件事件不等于文件已写完。编辑器保存常分两步:先写临时文件,再原子替换。直接打开刚创建的文件可能读到空内容或权限拒绝。
安全做法是加一层轻量级校验:检查文件是否存在、是否为普通文件、大小是否 > 0、是否有读权限。不要依赖 sleep 等待,那是竞态根源。
- 用
os.Stat()获取os.FileInfo,检查fi.Mode().IsRegular()和fi.Size() > 0 - 用
os.Open()尝试打开并立即Close(),捕获permission denied或text file busy错误 - 对频繁更新的目录(如日志目录),可加简单去重:10 秒内同一文件名只处理一次,用
map[string]time.Time缓存时间戳
并发扫描时如何避免文件句柄泄漏和 goroutine 泛滥
每个文件事件起一个 goroutine 扫描很危险:高频写入(如批量下载、日志轮转)会导致 goroutine 数飙升,且未关闭的 *os.File 会耗尽系统句柄。
必须控制并发度,并确保资源释放。别用无缓冲 channel 直接塞文件路径——那只是把问题从 goroutine 转移到 channel 阻塞上。
- 用带缓冲的 worker pool:固定 N 个 goroutine 从 channel 消费任务,N 建议设为 CPU 核心数或略高(如 4–8)
- 扫描函数内部必须用
defer f.Close()关闭文件,且放在os.Open成功之后 - 对大文件(>10MB),考虑用
io.LimitReader(f, 10 限制读取量,防止 OOM - 超时控制用
context.WithTimeout包裹整个扫描逻辑,避免卡死
扩展名列表如何热更新而不重启服务
硬编码扩展名列表(如 []string{".pdf", ".docx"})无法应对策略动态调整。需要支持运行时加载配置,且不中断监听。
关键是用原子变量替换全局切片,而不是加锁读写 map —— 多数场景只需查表,不需要增删键。
- 定义
var allowedExts = sync.AtomicValue[[]string]{}(Go 1.21+)或用sync.RWMutex保护普通 slice - 提供 HTTP 接口(如
POST /config/exts)接收 JSON 数组,解析后调用allowedExts.Store() - 过滤逻辑中每次用
allowedExts.Load()获取最新切片,用strings.HasSuffix或预建map[string]bool查找 - 注意:配置变更后,正在扫描的文件不受影响,这是预期行为;新事件才走新规则
扩展名过滤本身很简单,真正的复杂点在于事件时序、文件状态漂移和资源生命周期管理。盯着 fsnotify 的 event 类型和 os 层文件状态,比堆功能更重要。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











