Go channel底层无显式锁,但runtime.chansend/recv为串行临界区,保证FIFO与顺序语义;高并发时易成瓶颈,应依场景选用atomic、分片或ring buffer替代。

channel 本身不加锁,但 runtime.chansend/recv 是串行临界区
Go 的 channel 实现里没有用户可见的 sync.Mutex,但底层 runtime.chansend 和 runtime.chanrecv 函数内部对 channel 结构体的字段(如 sendq、recvq、buf)访问是原子且互斥的。多个 goroutine 同时向同一 channel 发送或接收时,实际在争抢的是这个 runtime 内部的“隐式锁”。这不是 bug,而是设计使然——channel 要保证 FIFO、无丢失、顺序语义,就必须串行化核心操作。
常见误判是:“没用 Mutex 就没锁竞争”,结果 pprof block profile 里 chan send 占比超 30%,QPS 卡在 2000 上不去,延迟毛刺明显,却还在查自己写的锁。
- 缓冲 channel 只是把阻塞点从“发送即等”延后到“缓冲满才等”,不消除临界区竞争
- 无缓冲 channel 在高并发写入场景下,所有发送 goroutine 会排队等 runtime 分配 recvq 节点,本质是单点调度瓶颈
- channel 关闭后,所有 pending send/recv 会被唤醒并失败,但关闭动作本身也需原子更新 channel 状态,同样参与竞争
什么时候该放弃 channel,改用 atomic 或分片结构
不是所有“通信”都该走 channel。当 profiling 显示 runtime.chansend 或 runtime.chanrecv 在 block profile 中高频出现,且业务逻辑只是做计数、状态标记、指标聚合这类简单变更时,channel 就成了过度设计。
例如:HTTP 请求计数器、错误码统计、活跃连接数跟踪——这些场景用 atomic.AddInt64 替代 channel send,开销从 ~200ns 降到 ~10ns,且完全无阻塞、无 goroutine 挂起风险。
- 读多写少的共享状态(如配置快照、缓存命中率)优先用
sync.RWMutex,而非通过 channel 广播更新 - 需要聚合 N 个 goroutine 结果时,避免让它们全往一个
results chan int里塞;改用局部变量 +sync.WaitGroup+ 最后一次汇总 - 日志上报若只是打点(非强一致),用 ring buffer + worker goroutine 批量刷出,比百个 goroutine 直接写
logCh chan string更稳
缓冲大小不能拍脑袋定,得按背压窗口算
设 ch := make(chan int, 1000) 不代表性能更好,只代表你把“阻塞”藏得更深了。真正要防的是生产者远快于消费者导致的积压爆炸。缓冲大小应基于可容忍的最大延迟 × 峰值吞吐来反推。
比如监控数据每秒上报 500 条,单条处理耗时平均 10ms,那瞬时积压上限是 500 × 0.01 = 5 条;乘以 2~3 倍安全系数,缓冲设为 16 或 32 更合理。设成 1000,只会让问题延迟暴露,OOM 前都看不出 consumer 掉队。
- 突发型任务(如批量埋点)可用
10–100,但必须配合select { case ch防写满卡死 - 日志类可设大缓冲(如 1000+),但写入端必须用
select+default,且default分支要有明确降级(落盘 or 丢弃并告警) - 绝对不要在循环里无 sleep 地重试
select { case ch—— 这是 CPU 忙等,不是背压控制
select + default 不是银弹,它解决的是阻塞,不是吞吐失衡
select { case ch 确实能防止 goroutine 挂起,但它把“阻塞”转化成了“丢弃”或“重试压力”。如果大量 goroutine 都在 default 分支里丢消息,说明系统已过载,此时该做的是限流、降级或扩容,而不是靠非阻塞掩盖问题。
更危险的是把 default 写空:goroutine 不挂起,也不处理,等于静默失败。线上出问题时连日志都没有。
-
default分支必须有副作用:记录 metric、触发告警、写本地文件、或 fallback 到 sync.Pool 备用路径 - 若需保序或不丢,
select应配合context.WithTimeout控制整体等待时间,而不是依赖 channel 自身行为 - 高吞吐下,频繁
select本身也有开销;当 channel 成为瓶颈时,不如直接换方案(如 ring buffer + worker)
真实瓶颈往往不在你写的锁上,而在 runtime 对 channel 的串行化保护里。一旦发现 runtime.chansend 在 block profile 中扎堆,就该立刻停掉“调大缓冲”这种直觉反应,转而问:这数据真需要 channel 传递吗?能不能用原子操作、分片、局部聚合绕过去?











