用 channel 做队列天然协程安全但限制多:无长度查询、不可遍历、不能 Peek、关闭后不可写,阻塞易卡死;常见错误是 select 缺 default 或向已关闭 chan 写入 panic;适用固定容量的生产者-消费者模型。

用 channel 做队列,天然协程安全但有隐含限制
直接用 chan 当队列最省事,send 和 recv 操作本身是原子的,不用加锁。但它不是“通用队列”——没有长度查询、不能遍历、不能 Peek、关掉后无法再写入,而且阻塞行为容易卡死 goroutine。
常见错误现象:select 非阻塞读取时漏掉 default 分支,导致无限等待;或者往已关闭的 chan 写入,panic 报 send on closed channel。
- 适用场景:生产者-消费者模型明确、容量固定(如
make(chan int, 10))、不需要中间状态检查 - 别用
len(ch)判断是否为空——它只返回缓冲区当前长度,不反映是否有 goroutine 正在阻塞等待读/写 - 如果需要“尝试入队但不阻塞”,得用
select+default,而不是靠len()判断
用 sync.Mutex + slice 实现可控制的并发队列
想支持 Len()、Peek()、动态扩容、批量操作?就得自己封装。核心是把所有访问都包在 mutex.Lock()/mutex.Unlock() 里,但锁粒度要小心——比如 Pop() 里先取值再删,不能拆成两段加锁。
容易踩的坑:append() 可能导致底层数组重分配,如果多个 goroutine 同时操作同一 slice,即使加了锁,也可能因逃逸或指针共享引发数据竞争(尤其在测试中偶发)。
立即学习“go语言免费学习笔记(深入)”;
- 必须把整个操作逻辑(读+改+写)放在单次锁区间内,例如
Pop()不能先Lock读出元素,Unlock,再Lock删除 - 避免返回内部 slice 元素的指针或引用,防止外部绕过锁修改底层数据
- 如果队列很长且读写频繁,考虑用
sync.RWMutex,但注意写操作仍需独占锁,读多写少才划算
sync.Pool 不适合做队列,但能缓解高频小对象分配压力
有人想用 sync.Pool 缓存节点结构体来加速队列操作,这是误解。sync.Pool 是无序、不可预测回收的临时对象池,不保证对象复用时机,也不能控制生命周期,完全不适合作为队列的数据容器。
但它可以配合手动内存管理:比如你的队列节点是 struct{ val int; next *node },每次 Pop() 后把节点 Put() 进池子,下次 Push() 优先从池子 Get(),减少 GC 压力。
- 仅适用于节点生命周期清晰、不会跨 goroutine 长期持有指针的场景
- 池子里的对象可能被任意时间清理,必须在
Get()后做有效性检查(比如清空字段) - 别对池子做
Len()或统计,它不提供这些能力
第三方库如 container/list 或 golang.org/x/exp/constraints 并不自动协程安全
container/list 是标准库双向链表,但它没加任何锁——所有方法都不是并发安全的。直接在多个 goroutine 里调用 list.PushBack() 或 list.Front(),必然触发 data race,go test -race 一跑就报。
泛型版本(如带 constraints.Ordered 的队列实现)也一样:类型安全 ≠ 并发安全。泛型解决的是类型擦除问题,不是同步问题。
- 如果要用
container/list,必须外层套sync.Mutex,且锁范围覆盖所有依赖链表状态的操作 - 别迷信“标准库”或“泛型”等于“开箱即用并发安全”,Golang 的哲学是显式优于隐式
- 真要选第三方队列库,盯紧文档里是否明确写了 “safe for concurrent use” —— 大部分没写的就是不安全
真正难的不是加锁,而是判断哪些操作必须原子、哪些状态变更不可分割。比如一个 TryPop() 方法,既要返回值、又要更新长度、还要移动头指针——这三件事必须在一个锁里做完,少一个环节,就埋下竞态的种子。










