无缓冲 channel 并非真正同步,而是协程间握手式阻塞:发送方必须等待接收方就绪才能继续,本质是同步阻塞而非高效同步。
无缓冲 chan int 确实强制发送与接收 goroutine 同时就绪,但“同步”不等于“高效”。它本质是协程间的一次握手:发送方卡在 ch ,直到接收方执行到 才继续。这在控制信号(如 done 通知)、状态切换等场景很干净;但一旦生产者或消费者稍有延迟,整条流水线就卡住。
make(chan struct{});要吞吐,就得给缓冲go tool pprof -goroutines 查看阻塞在 chan send 或 chan recv 的 goroutine 数量,飙升就是同步瓶颈信号网上流传的 bufferSize = (生产速率 − 消费速率) × 最大容忍延迟 是理论下限,实际必须叠加内存和背压策略。比如日志系统中,你算出要 800,但若单条日志 1KB,缓冲区就占 800KB 内存——而你的边缘设备只有 2MB 可用堆,这就不可行。
make(chan *LogEntry, 16) 起步,用 ab 或 hey 模拟 2× 峰值流量,观察 runtime.ReadMemStats().HeapAlloc 和 channel 阻塞率(通过 debug.ReadGCStats 间接估算)缓冲区不是越大越好。实测发现:当 make(chan int, 10000) 时,goroutine 在写满后首次阻塞的耗时,比 make(chan int, 100) 高出 3–5 倍——因为 runtime 要遍历整个环形缓冲区判断是否 full,而底层 hchan 的 sendq 队列查找开销随长度非线性增长。
pprof trace 显示大量时间花在 chan.send
sync/atomic;对中低频大数据(如图片帧),缓冲区上限建议 ≤ 1024,再大就拆成多个 channel + 负载均衡 goroutinerange 遍历带缓冲 channel 时,如果生产者未 close,消费者会永远等不到 EOF——这和无缓冲 channel 行为一致,但容易被忽略线上服务里,光选对缓冲区不够,得防住缓冲区满、goroutine 泄漏、channel 关闭混乱这三类高频事故。
select {
case ch <- item:
default:
metrics.Counter("channel_dropped").Inc()
// 或 return errors.New("channel full")
}context.WithTimeout 包裹,超时后 close(ch) 并 returnvalue, ok := 判断是否 closed,别依赖 for range 自动退出(万一生产者忘了 close 就死锁)
缓冲区选择本质是吞吐、延迟、内存、稳定性四者的权衡点,没有银弹。最危险的不是选错数字,而是选完就不管——定期用 go tool pprof -http=:8080 看 channel 阻塞热图,比任何理论都管用。