Go 链表简介
Go 标准库里没有单链表,只在 container/list 包里提供了双向循环链表。
两个核心类型
list.List :链表本身,包含哨兵节点和长度
list.Element :链表节点,存数据 + 前后指针
Go
type Element struct {
Value interface{} // 节点数据 (任意类型)
// next, prev 是私有字段,不能直接访问
}
特点
• 双向:每个节点有 Next() 和 Prev(),支持双向遍历
• 循环:内部用一个哨兵节点串成环(首尾相连),所以代码里没有 nil 判断的边界 case,操作非常简洁
• Value 是 interface{}:可以存任意类型,但取出来要类型断言(e.Value.(int))
• Len() 是 O(1):内部维护了计数,不需要遍历
常用方法速查

遍历模板
Go
package main
import (
"container/list"
"fmt"
)
func main() {
// 创建一个新的链表
l := list.New()
// 在链表的尾部添加元素
l.PushBack("Go")
l.PushBack("is")
l.PushBack("awesome")
// 在链表的头部添加元素
l.PushFront("Programming")
// 遍历链表并打印元素
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
几个坑
-
Value 类型断言:e.Value.(int),类型错了会 panic
-
查找是 O(n):链表没有按值查找的 API,得自己写循环 → 这就是为什么 LRU 要配一个 map 做索引
-
节点必须先拿到 *list.Element 才能操作:Remove / InsertBefore / MoveToFront 都接收节点指针,不接收值
-
不要跨链表移动节点:l1 的节点别拿去 l2.Remove(),行为未定义
什么时候用 container/list
• ✅ 需要频繁在中间插入/删除且已经持有节点指针(LRU 缓存、任务调度队列)
• ❌ 只是顺序存取 / 随机访问 → 直接用 slice,cache 友好性吊打链表
99% 的场景用 slice 就够了,剩下 1% 才考虑 container/list。
LRU 示例
Go
package main
import (
"container/list"
"fmt"
)
// 实现一个LRU,缓存策略,缓存满了就淘汰最久没使用的那个
type LRUCache struct {
capability int
ll *list.List
cache map[int]*list.Element
}
type entry struct {
key, value int
}
func Constructor(capability int) LRUCache {
return LRUCache{
capability: capability,
ll: list.New(),
cache: make(map[int]*list.Element),
}
}
// 存元素,如果元素已经存在,则更新,放到头部;
// 如果元素不存在,则加到头部,然后判断是否溢出。
func(c *LRUCache) Put(key, value int) {
if e,ok := c.cache[key]; ok{
e.Value.(*entry).value = value
c.ll.MoveToFront(e)
return
}
e := c.ll.PushFront(&entry{key: key, value: value})
c.cache[key] = e
if c.ll.Len() > c.capability {
e := c.ll.Back()
delete(c.cache, e.Value.(*entry).key)
c.ll.Remove(e)
}
}
// 获取元素,如果元素存在放回,并放回头部;如果不存在,返回-1
func(c *LRUCache) Get(key int) int {
if e, ok := c.cache[key]; ok {
c.ll.MoveToFront(e)
return e.Value.(*entry).value
}
return -1
}
func main() {
lrucache := Constructor(2)
lrucache.Put(1,3)
lrucache.Put(1,4)
lrucache.Put(2,9)
for e:=lrucache.ll.Front(); e!=nil;e=e.Next() {
fmt.Println(e.Value.(*entry).value)
}
}
QA1:为什么用指针接收者 c *LRUCache
因为方法里要修改 c 的内部状态(链表、map),用值接收者会改了个寂寞。
对比
Go
// 值接收者:c 是副本,改了不影响原对象
func (c LRUCache) Put(...) { c.ll.PushFront(...) } // 🤔 看起来生效了?
// 指针接收者:c 指向原对象,改的就是它本身
func (c *LRUCache) Put(...) { c.ll.PushFront(...) } // ✅ 真正生效
⚠️ 有个细节:ll 本身是 *list.List(指针),所以值接收者下 PushFront
居然也"看似"能改链表------因为副本和原对象共享同一个底层链表。但 capacity 这种字段就改不动了,而且 map 操作也会出问题。不要依赖这种巧合。
Go 的两条经验法则
-
方法里要改字段 → 必须用指针接收者
-
结构体比较大、或包含 mutex/slice/map → 也建议用指针(避免拷贝、避免锁失效)
-
同一个类型的所有方法,接收者类型要统一(要么都是值,要么都是指针),否则方法集混乱、接口实现也容易踩坑
LRU 三条全占:要改状态、含 map 和 list、需要保持一致 → 必须是 *LRUCache。
QA2: *list.List为什么是指针
因为list.New()返回指针,
QA3: *list.Element 为什么这个也是指针呢
因为 container/list 里所有方法本来就要求传 *list.Element,map 存指针只是顺手保持一致,避免来回取址。
看 list 包的 API 签名:
Go
func (l *List) PushFront(v any) *list.Element // 返回的就是指针
func (l *List) MoveToFront(e *list .Element) // 接收指针
func (l *List) Remove(e *list.Element) any // 接收指针
func (l *List) InsertBefore(v any, mark *list.Element) // 接收指针
Go 标准库设计上,链表节点天生就是用指针在传递的。所以:
Go
elem := c.ll.PushFront(...) // elem 已经是 *list.Element
c.cache[key] = elem // 直接塞 map,类型匹配
如果存值类型,每次用还得 &v,自找麻烦。
QA4: *list.List不是一个双向链表吗,一直next(),为什么会出现nil呢
container/list 的实现确实是循环双向链表,内部有一个哨兵节点(root),最后一个节点的 next 指向 root,root 的 next 又指向第一个节点。但是 Next() 和 Prev() 这两个公开方法被故意做了处理:当下一个节点是 root 哨兵节点时,返回 nil,而不是把这个内部节点暴露出来。
标准库源码(src/container/list/list.go):
Go
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
关键就是 p != &e.list.root 这个判断------一旦走到 root 哨兵,就返回 nil。
所以:
• 内部数据结构:循环双向链表(用 root 哨兵简化插入/删除的边界判断,不需要处理 head/tail 为 nil 的特殊情况)。
• 对外暴露的迭代接口:非循环,到末尾返回 nil,符合 Go 的习惯写法: