直接用 github.com/hashicorp/golang-lru/v2,它已解决并发、指针同步、淘汰顺序错乱等 panic 问题;手写 container/list + map 易因生命周期与并发模型不匹配导致静默丢数据或 crash。

别手写 LRU 缓存结构体——直接用 github.com/hashicorp/golang-lru/v2,它已解决并发、指针同步、淘汰顺序错乱等所有典型 panic 场景;自己基于 container/list + map 实现,在高并发或容量临界时大概率静默丢数据或直接 crash。
为什么手写 container/list + map 会 panic 或丢数据
这不是逻辑写错的问题,而是生命周期和并发模型根本没对齐:
-
list.Remove(e)后仍保留e.Value引用,后续解引用可能触发nil pointer dereference - 多个 goroutine 同时
Get同一个 key,竞态调用list.MoveToFront(),prev/next指针被并发改写,报错assignment to entry in nil map - 淘汰时先
delete(m, key)再list.Remove(e),导致map中残留 key 指向已释放节点,后续Get返回零值却不报错——数据静默丢失
golang-lru/v2 的 New 和 Add 必须这样用
它默认线程安全,但类型擦除和覆盖语义容易踩坑:
- 泛型参数必须显式指定,例如
lru.New[string, int](128);写成lru.New[any, any]会导致Get返回any,类型断言失败时静默返回零值 -
Add(key, value)是覆盖写入:key 存在时自动更新 value 并重置访问序,无需先Remove再Add - 容量满时自动淘汰
list.Back()对应项(最久未使用),不可配置;若需驱逐通知,必须传onEvicted函数,且该函数内严禁阻塞操作(如 HTTP 调用)
Resize 不是扩容,是重建,会影响访问热度感知
cache.Resize(256) 看似简单,实际会阻塞并重建整个内部结构:
- 新建容量为 256 的链表,把当前存活 key-value 按访问序迁移过去,旧结构交由 GC 回收
- 迁移全程持有写锁,期间所有
Get和Add都会阻塞;若缓存已有上万条目,耗时可达毫秒级 - 迁移后所有 key 的访问热度清零,刚进来的 key 全被视为“最近使用”;如果你依赖历史访问频次做降级(比如只对访问 ≥3 次的 key 预热),Resize 后该逻辑彻底失效
真正难的不是实现 MoveToFront,而是让 map 删除、链表移除、指针归零、类型安全、并发控制这五件事在任意时刻都严格同步——这些细节藏在每一行 delete 和每次 list.Remove 的顺序里,稍一错位,缓存就从加速器变成定时炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











