
channel 缓冲区大小设为 0 时,发送必然阻塞
零缓冲 channel(make(chan int))是同步的:发送方必须等到有 goroutine 在另一端 receive 才能继续。这不是“延迟高”,而是**确定性等待**——它把协程调度、内存可见性、临界区保护全交给了 Go 运行时调度器。常见错误是误以为“不加缓冲就快”,结果在高并发写日志、上报指标等场景中,send 卡住整个 goroutine,拖慢上游逻辑。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 仅在需要严格配对(如 request-response、信号通知)时用
make(chan struct{}) - 避免在热路径(如 HTTP handler 内部)直接向无缓冲 channel 发送数据
- 用
select+default做非阻塞尝试,但要注意这会丢消息,不是“降级”而是“放弃”
缓冲区过大反而放大延迟尖刺
设成 make(chan int, 10000) 看似能吞下突发流量,但实际会掩盖背压缺失问题。当消费者处理变慢,缓冲区积压导致消息在 channel 里排队等待,延迟从毫秒级变成秒级,且无法感知——因为 send 仍成功返回。更糟的是,GC 会对大缓冲区里的元素做扫描,若存的是指针类型(如 *bytes.Buffer),会拖慢 STW 时间。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 缓冲区大小不是越大越好;优先用监控(如
len(ch)/cap(ch)比值)识别积压,而不是靠调大缓冲硬扛 - 若必须设缓冲,按 P99 处理耗时 × 预期峰值 QPS 粗略估算,再向下取整(比如 50ms × 200QPS = 10,那就设 8~16)
- 避免在缓冲 channel 中存大对象或带指针的结构体,否则 GC 压力陡增
用 len(ch) 查看积压时,结果只代表“那一刻”的快照
len(ch) 返回当前已入队但未被取走的元素个数,但它不是原子操作,也不锁 channel。在并发读写下,刚读完 len(ch) 是 100,下一毫秒可能就被消费掉 90 个,也可能又塞进 200 个。把它当监控指标可以,但绝不能用于控制逻辑(比如 “if len(ch) > 100 { drop }”)。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 监控告警可用
len(ch),但采样频率别太高(比如 1s 一次),避免频繁反射开销 - 不要用
len(ch)做流控判断;真要限速,用time.Ticker或令牌桶(golang.org/x/time/rate) - 调试时想确认是否积压,比
len(ch)更可靠的是看 goroutine stack:用runtime.Stack抓到正在chan send阻塞的 goroutine
关闭已满的缓冲 channel 会 panic
对一个缓冲区已满的 channel 调用 close(ch) 不会立即 panic,但只要还有 goroutine 尝试 send,就会触发 panic: send on closed channel。这个 panic 不在 close 时发生,而在下一次 send 时——容易误判为“close 后代码出问题”,其实根子在关闭前没清空或没控制好生产节奏。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 关闭 channel 前,确保所有生产者已退出,或用 sync.WaitGroup 协调
- 消费者侧统一用
for v := range ch,它会在 close 后自动退出,比手动recv+ok判断更安全 - 如果 channel 是多生产者,别依赖 close 作为终止信号;改用额外的
done chan struct{}控制生命周期
channel 的缓冲区不是性能开关,而是背压策略的显式表达。设多少、何时关、怎么监,都得贴着业务的数据节奏来——离了真实流量分布和消费者能力,光调数字没用。









