Go标准库不支持文件TTL,os.WriteFile无法直接写带过期时间的文件;过期需手动判断与清理,常见错误是误以为注释即生效。

为什么不能用 os.WriteFile 直接写带过期时间的文件
Go 标准库不支持给文件本身设 TTL —— os.WriteFile 写出的文件不会自动删除,os.Chtimes 改 mtime 也不会触发清理。所谓“过期”,全靠你手动判断和执行。常见错误是写完文件就以为“加了注释 // expires in 5m 就真会删”,结果缓存堆积、磁盘爆满。
文件缓存过期该存时间戳还是元数据文件
两种主流方式各有适用场景,别硬套:
- 小文件 + 结构化内容(如 JSON/YAML 配置):直接把
expires_at字段序列化进文件体,读取时json.Unmarshal后立刻校验item.ExpiresAt.Before(time.Now()) - 二进制文件(如图片、proto 缓存)或高频写入场景:配对生成
.meta文件,例如cache.bin对应cache.bin.meta,后者只存一行 Unix 时间戳;避免反序列化开销,也防止主文件损坏导致元数据丢失 - 超大量缓存(>10 万文件):放弃单文件元数据,改用内存
map[string]time.Time或轻量 SQLite 表统一索引路径 → 过期时间,启动时加载,后台定时扫描索引而非遍历目录
后台扫描清理必须分批 + 快照时间 + RWMutex
直接 for k := range cacheMap 遍历删文件,在大目录下会卡住、OOM、漏删。正确做法是:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
filepath.WalkDir扫目录,但每次只收集最多 200 个路径到 slice,避免一次性加载全部文件名进内存 - 扫描前调一次
now := time.Now(),所有fi.ModTime().After(now)判断都基于它,防止系统时间跳变或纳秒误差导致误判 - 删除操作前加日志:
log.Printf("removing expired file: %s", path);失败不 panic,continue下一个 - 如果缓存 map 是读多写少,清理函数用
mu.Lock(),读路径一律用mu.RLock()—— 别让清理阻塞每秒上万次的读请求
如何安全触发驱逐并释放关联资源
文件缓存淘汰不只是删文件,常需同步释放句柄、关闭连接、上报指标。golang-lru 的 OnEvict 回调能帮你解耦:
立即学习“go语言免费学习笔记(深入)”;
- 初始化时用
lru.NewWithEvict[string, *os.File],回调里做f.Close()和os.Remove(path) - 别在回调里做耗时操作(如 HTTP 调用),否则拖慢整个 LRU 驱逐流程;可发消息到 channel 交由 worker goroutine 处理
- 若缓存项是结构体(如
type CacheEntry { Path string; File *os.File; ExpiresAt time.Time }),确保OnEvict拿到的是完整值,不是指针别名 - 注意:
OnEvict只在容量淘汰时触发,不覆盖过期逻辑 —— 过期仍要靠 Get 时懒检查或后台扫描,二者互补,不是替代
OnEvict 里关错句柄会引发 panic。这些细节藏在每次 os.Stat 调用和每行 time.Now() 里,稍不注意,缓存就从加速器变成定时炸弹。

















