不能直接用os.openfile读写缓存文件,因存在并发写入错乱、无过期判断、非原子更新致读到半写状态三坑;正确做法是分文件存储、rwmutex控并发、临时文件+rename原子替换、filepath.join安全拼路径。

为什么不用 os.OpenFile 直接读写缓存文件
直接用 os.OpenFile 读写单个文件做缓存,看似简单,实际会踩三个坑:并发写入时文件内容错乱、没有过期判断逻辑、无法原子更新导致读到半写状态。缓存不是“存进去再读出来”就行,得保证一致性、时效性和线程安全。
正确做法是把每个缓存项拆成独立文件(key.hash),配合 sync.RWMutex 控制目录级并发,再用 os.Stat 检查 ModTime 判断是否过期。这样既避免锁粒度太粗,又绕开了文件锁的平台差异问题。
- 文件名用
fmt.Sprintf("%x", md5.Sum([]byte(key)))生成,避免特殊字符和路径穿越 - 写入前先写临时文件(
tmp_ + hash),os.Rename原子替换,防止读取中断 - 读取时先
os.Stat检查是否存在且未过期,再os.ReadFile,不缓存文件句柄
如何让 cache.Get 返回类型安全的值而不是 []byte
Go 没有泛型运行时擦除,硬编码 interface{} 会导致每次都要类型断言,还容易 panic。更稳妥的方式是把序列化逻辑下沉到调用方,缓存层只管存/取 []byte,由上层决定用 json.Marshal 还是 gob.Encoder。
示例中定义 type Cache interface { Get(key string) ([]byte, error); Set(key string, data []byte, ttl time.Duration) error },使用者按需封装:
func (c *FileCache) GetString(key string) (string, error) {
b, err := c.Get(key)
if err != nil {
return "", err
}
return string(b), nil
}
func (c *FileCache) GetJSON(key string, v interface{}) error {
b, err := c.Get(key)
if err != nil {
return err
}
return json.Unmarshal(b, v)
}
- 不要在缓存内部做
json.Unmarshal—— 错误堆栈会指向缓存代码而非业务层 - 如果大量使用结构体,建议统一用
gob(比 JSON 小且快),但注意gob不兼容跨版本 Go 运行时 - 对字符串/数字等简单类型,直接存
[]byte最省事,避免序列化开销
filepath.Join(cacheDir, hash) 为什么不能省略 filepath.Join
有人图省事拼字符串:cacheDir + "/" + hash,这在 Windows 下直接失效 —— 路径分隔符是 \,而且 cacheDir 结尾可能带或不带斜杠,拼出来可能是 C:\cache//abc123 或 /tmp/cache\abc123,os.Stat 一律返回 stat cache//abc123: no such file。
-
filepath.Join自动处理不同系统的分隔符,并清理重复斜杠和首尾分隔符 - 务必对
cacheDir做filepath.Clean预处理,防止传入../etc/passwd类路径穿越参数 - 初始化时用
os.MkdirAll(cacheDir, 0755)创建目录,否则首次Set会因父目录不存在而失败
缓存清理策略:不依赖定时器,靠 Set 时顺手删过期文件
为轻量级缓存单独起 goroutine 跑定时清理,既增加复杂度又浪费资源。实际观察发现,90% 的过期文件是在 Set 新值时被覆盖或自然淘汰的 —— 更合理的做法是在每次 Set 前扫描并删除一批过期文件(比如最多删 10 个),用 time.Now().Add(-ttl) 对比 os.FileInfo.ModTime()。
- 扫描范围限制在当前目录下,不递归子目录,避免意外删错
- 用
os.ReadDir(非filepath.Walk)提升性能,它不 stat 每个文件,只读目录项 - 删除失败(如权限不足)就跳过,不中断主流程,日志里记一条 warning 即可
真正难处理的是磁盘空间耗尽——这时候连临时文件都写不了。Set 必须检查 os.WriteFile 返回的 syscall.ENOSPC,并主动触发一次强制清理(扫全量 + 删除最老的 N 个),否则缓存会彻底卡死。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











