不能直接用sync.map存[]byte,因其不支持ttl、无内存可见性保证、并发读可能看到未初始化字段,且超几百项应换golang-lru;需封装含writetime/etag的结构体并每次load时惰性过期校验。

为什么不能直接用 sync.Map 存 []byte
很多人一上来就把文件内容 []byte 直接 Store 进 sync.Map,结果跑几小时 RSS 暴涨、返回陈旧数据。根本原因有三个:
— sync.Map 不支持 TTL,没法自动淘汰过期项;
— 它不保证结构体字段的内存可见性,多个 goroutine 同时读可能看到未初始化完成的 etag 或 writeTime;
— 当你缓存的是带偏移的文件块(比如 Range: bytes=100-199),Load 返回的是指针,字段初始化顺序和读取时机错位会导致 panic 或脏读。
必须封装成含元信息的结构体再缓存
别存裸 []byte,定义一个明确字段的结构体,把关键元信息收拢进来:
type CachedFile struct {
Data []byte
ETag string
WriteTime time.Time
Size int64
}
这样做的好处是:
— 所有字段一次写入、原子可见,避免并发读到半初始化状态;
— ETag 和 WriteTime 可用于后续 If-None-Match 或 If-Modified-Since 校验;
— Size 避免每次调 len() 或 os.Stat();
— 过期判断逻辑统一收在 Get 里,而不是靠后台 goroutine 清理——后者漏判率高,尤其在低频访问场景下。
缓存 key 必须包含语义上下文
只用 r.URL.Path 做 key,会把 Accept: application/json 和 Accept: text/html 的响应混在一起。正确做法是拼接完整语义标识:
r.Method + r.URL.Path + r.URL.RawQuery+ r.Header.Get("Accept") + r.Header.Get("Range")
这样能区分同一路径下的不同格式、分片请求。非 GET/HEAD 请求直接跳过缓存,避免 POST 结果被意外复用。
超过几百项缓存就该换 golang-lru
sync.Map 在 key 数量少、写入稀疏时表现好,但一旦缓存项超几百个,就会暴露两个硬伤:
— 无法限制容量,容易吃光内存;
— 没有 OnEvicted 回调,没法在淘汰时释放 mmap 句柄或 close 文件 fd。
换成 github.com/hashicorp/golang-lru 后:
— 可设固定容量(如 lru.New(1024));
— 淘汰时触发回调,安全释放资源;
— 内置 LRU 算法,比手写淘汰逻辑更稳。
WriteHeader 必须和 Write 一起拦截——只重写 Write 会丢掉 500、404 这类状态码,导致错误响应被当成 200 缓存进去。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











