带缓冲channel能替代读写锁,因其天然序列化写入操作、避免竞态且提升吞吐;但需显式设置缓冲容量,零值chan无缓冲会立即阻塞。

直接用锁保护共享资源在高并发下必然成为瓶颈,真正有效的解法不是“优化锁”,而是绕过锁——用队列把竞争转为串行化处理,让写操作收敛到单个 goroutine。
为什么带缓冲 channel 能替代大部分读写锁
很多场景里所谓“并发安全”,本质只是要求「多个生产者不能同时改同一块内存」。而 chan 天然提供这种顺序保障:写入操作被 runtime 底层序列化,无需额外加锁。
- 用
make(chan string, 1024)替代sync.RWMutex保护一个日志字符串切片,吞吐能翻倍,P99 延迟更稳 - 缓冲容量必须显式设置,零值
chan string是无缓冲的,一写就阻塞,等于把压力甩给调用方 - 别在
select里只写case ch 就完事,一定要配 <code>default:或timeout,否则突发流量会卡死上游 - channel 关闭后继续写会 panic,但读取会得到零值——这个行为要主动 check,不能靠 recover
sync.Pool + channel 组合防 GC 和内存抖动
高频写入字符串或结构体时,光用 channel 不够,新分配对象会触发 GC,进而拉高延迟。关键是要复用内存,而不是反复 new。
- 定义
var msgPool = sync.Pool{New: func() interface{} { return &Message{} }},其中Message是你实际传输的结构体 - 发送前调用
m := msgPool.Get().(*Message),填充字段后ch ;消费者处理完立刻 <code>msgPool.Put(m) - 注意:不要在
Put后还访问该对象字段,Pool 不保证对象内容清空,可能拿到脏数据 - 字符串尤其危险:
string转[]byte默认逃逸到堆,若底层缓冲区是make([]byte, 4096)并长期持有,得用unsafe.String避免重复分配
Worker pool 必须配 WaitGroup + context 控制生命周期
不加约束的 worker 启动容易,收尾却极难——goroutine 泄露、channel 关闭竞态、context 取消后还在读写,都是线上高频问题。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 启动时用
sync.WaitGroup记录 worker 数量,每个 worker 在 for 循环外defer wg.Done() - worker 内部用
for { select { case msg, ok := ,不能只依赖 channel 关闭 - 关闭流程必须是:先
close(ch),再wg.Wait(),最后才释放资源(比如关闭 DB 连接) - 别用
for range ch简写——它隐式忽略 channel 关闭信号,ctx.Done()到来时无法及时退出
RingBuffer 不是万能解,但比 channel 更适合超低延迟场景
当 P99 延迟压到 100μs 级别,甚至要求“生产者绝不能等消费者”,chan 的 runtime 调度开销就开始暴露。这时 RingBuffer 才值得考虑,但代价是复杂度陡增。
- 容量必须是 2 的幂(如 1024),否则
idx & (cap-1)绕回计算错位,静默覆盖数据 - 写入必须用
atomic.CompareAndSwapUint64重试,atomic.AddUint64会导致多 goroutine 写进同一个 slot - 字符串写入前必须避免逃逸:底层缓冲区要用
make([]byte, cap*slotSize)一次性分配,并绑定整个生命周期 - 别自己手写 RingBuffer —— 它比看起来危险得多,
go.uber.org/zap和github.com/valyala/bytebufferpool的实现已足够健壮
最常被忽略的一点:队列不是银弹,它把锁竞争转成了调度竞争。如果 worker 处理逻辑里有 HTTP 请求或 DB 查询,那队列只是把 futex 等待换成了 goroutine 阻塞等待——问题没解决,只是藏得更深了。

















