最稳妥组合是sync.Mutex+container/list.List+map[string]*list.Element;因其封装链表指针操作、避免裸指针误用,配合统一锁保护所有读写与淘汰逻辑,能可靠实现原子性与并发安全。

直接上结论:手写 Go LRU 缓存,sync.Mutex + container/list.List + map[string]*list.Element 是最稳妥、最容易避开坑的组合。别抄“纯指针双向链表”实现——90% 的 panic 和内存泄漏都出在节点指针管理错位上。
为什么不用自己写双向链表节点?
自己定义 type node struct { key string; value interface{}; prev, next *node } 看似可控,实则极易出错:
-
delete(m, key)后忘记置空e.prev.next = e.next和e.next.prev = e.prev,导致链表断裂或迭代 panic - 多个 goroutine 并发调用
Get同一个未命中 key,会同时执行Put流程,可能重复插入同一 key,造成 map 冲突或链表节点重复挂载 -
list.Remove(e)后继续读取e.Value—— 此时e已从 list 中解绑,e.Value不再受 list 生命周期保护,但 Go 不报错,容易产生静默脏数据 - 哨兵节点(
head/tail)初始化漏掉head.next = tail或tail.prev = head,后续addToHead直接 panic: invalid memory address
为什么 container/list.List 更安全?
container/list.List 是标准库封装好的双向链表,所有指针操作已内聚,暴露的是 *list.Element 抽象,避免裸指针误操作:
- 插入/删除/移动都通过方法调用(
l.PushFront()、l.MoveToFront()、l.Remove()),不会暴露prev/next -
list.Element.Value是interface{},但只要你在Put时统一构造&CacheValue{key: k, value: v},就规避了类型断言失败丢值问题 - 必须配
sync.Mutex:因为list.List本身是线程不安全的,但锁粒度可控制在方法入口,比自己维护指针锁更清晰 - 淘汰逻辑必须严格按顺序:
element := l.Back(); l.Remove(element); delete(m, element.Value.(*CacheValue).key)—— 反过来先delete再Remove,会导致 map 中残留已失效的*list.Element,下次访问触发 panic: assignment to entry in nil map
并发 Get 场景下怎么避免重复加载?
标准 LRU 不解决缓存击穿,但你可以低成本加一层保护:
立即学习“go语言免费学习笔记(深入)”;
- 在
Get中查 map 命中前,先mu.Lock();若未命中,**不要立即解锁去加载**,而是先mu.Unlock(),用sync.Once或单 flight group 包裹实际加载逻辑 - 或者改用
sync.Map存 key → loading state(如map[string]chan struct{}),但注意:这仅防重复加载,不改变 LRU 排序逻辑 - 切忌在
Get里边mu.Lock()边调用外部 HTTP/DB —— 锁持有时间过长,直接拖垮吞吐 - 测试必须覆盖“容量为 2,连续 Put(a), Put(b), Put(c),再 Get(a)”:正确行为是返回 nil,而非 panic 或返回旧值
真正难的不是写对一次 Get/Put,而是让 MoveToFront、Remove、delete 这三步在并发和边界条件下始终原子且顺序一致。哪怕只漏掉一个 mu.Lock() 或写反一行删除顺序,上线后都可能表现为偶发 panic 或缓存数据静默丢失——这种问题在线上很难复现,但压测时一定会爆。


















