Go官方不鼓励手写无锁队列,因易出错、难验证且多数场景反而更慢;sync.Mutex+container/list已足够,95%场景无需真正无锁,推荐优先使用优化的buffered chan或成熟第三方ringbuffer。

sync.Mutex + container/list 就够用,95% 的场景不需要真正无锁队列。
Go 官方不鼓励手写无锁队列,不是因为技术上做不到,而是它容易出错、难验证、且多数时候反而更慢。
为什么 atomic.CompareAndSwapPointer 不能直接拼出安全队列
- ABA 问题无法自动规避:指针被回收又复用,CAS 看似成功,实际已指向无效内存
- GC 可能在节点被 CAS 引用后立即回收它,尤其当没用
runtime.KeepAlive或//go:uintptr标记时 - 内存重排未显式控制:Go 1.20 前不支持
atomic.LoadAcquire/atomic.StoreRelease,仅靠CompareAndSwapPointer无法保证 head/tail 协同更新的可见性顺序 -
atomic.Value不适用:它只支持原子替换整个值,不能做链表节点插入/删除,也无法协调两个指针(head 和 tail)的原子联动
常见错误现象:压测几分钟后 panic: "invalid memory address",或静默丢数据、死循环、len 返回负数。
chan 是最接近“无锁语义”的生产选择
-
make(chan T, N)底层是带锁环形缓冲,但经过 runtime 深度优化,实测吞吐常高于手写无锁链表 - 它天然支持非阻塞写:
select { case ch ,比加锁再判断是否满更轻量 - 避免用
len(ch)判断可写性——它只反映当前缓冲长度,不承诺下一次操作不阻塞 - 大结构体传递务必用
chan *T,并确保对象来自sync.Pool或生命周期可控的固定内存池 - 关闭 channel 后继续读会返回零值,掩盖业务逻辑错误;建议用
select+ok显式判别
注意:channel 不支持 peek 或 “退回” 操作。需要预览+重试语义?老实用 sync.Mutex 包一层 container/list,加个 Peek 方法和 Rollback 方法即可。
立即学习“go语言免费学习笔记(深入)”;
真要无锁?只用成熟第三方 ringbuffer
-
github.com/panjf2000/gnet/ringbuffer:纯 Go,显式处理 GC barrier 和内存序,支持Read/Write分离,适合 SPSC 场景 -
github.com/cilium/ebpf/internal/lockfree:SPSC ring buffer,零分配、零 GC,但属于内部包,需自行提取并注意 license -
github.com/Workiva/go-datastructures/queue/atomicqueue:MPMC,基于 Michael-Scott,但要求元素为指针类型且禁止 GC 移动(需//go:uintptr)
自己实现 MPMC 无锁队列几乎必然踩坑:ABA、cache line bouncing、32 位溢出截断、指针悬空……这些在真实长周期服务中会以偶发 panic 或延迟毛刺形式出现,极难复现和定位。
真正难的不是写一个能跑通的无锁队列,而是在 GC、调度器抢占、NUMA 架构、不同 CPU cache coherency 协议下,保证它永远不出错。绝大多数项目卡在这里,不是因为不会写 CAS,而是不知道什么时候该停手。

















