无缓冲 channel 会导致 pipeline 卡死,因其同步语义强,发送/接收未就绪即阻塞;应使用带缓冲 channel(如 make(chan Job, 10)),由唯一生产者关闭,下游 range 自动退出。

为什么 chan T 直接做 pipeline 会卡死
因为无缓冲 channel 在发送和接收未就绪时会阻塞 goroutine,一旦某段处理慢了(比如下游 range 消费不及时),上游就会被挂起,整个 pipeline 停摆。这不是并发没生效,而是同步语义太强。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有中间 stage 的输入/输出 channel 都该用带缓冲的
make(chan T, N),缓冲大小不是越大越好,得看处理延迟波动范围 - 典型值:如果单个任务平均耗时 10ms、峰值 50ms、QPS 约 200,缓冲设为
make(chan Job, 10)足够扛住短时抖动 - 别用
cap(ch) == 0来判断是否缓冲——它只反映 make 时的 cap,运行时无法动态查;要靠设计约定或文档说明
怎么安全地关闭带缓冲 channel 的 pipeline
直接 close(ch) 不等于下游能立刻退出 range:缓冲里还有数据没读完,range 会等清空才结束;更糟的是,多个 goroutine 同时 close 同一个 channel 会 panic。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 只由**唯一生产者**负责 close 输入 channel(比如第一个 stage 的 goroutine)
- 下游用
for job := range ch自然退出,别自己加select { case 混合逻辑 - 如果需要提前中断(如超时),用额外的
done chan struct{}配合select,但不要 close 工作 channel
sync.WaitGroup 和 context.Context 该选哪个来协调 pipeline
WaitGroup 只管“是否全做完”,不管“中途出错要不要停”;Context 天然支持取消、超时、传递 deadline,但滥用会导致每个 stage 都要检查 ctx.Done(),代码变啰嗦。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 纯离线批量处理(如日志解析)、无外部依赖、不关心中断——用
WaitGroup更轻量 - 涉及 HTTP 调用、数据库、用户请求响应——必须用
Context,且每个 stage 的阻塞操作(如http.Do、db.Query)都要传入ctx - 别把
Context当成WaitGroup的替代品:它们解决的问题不同,常一起用——WaitGroup等 goroutine 结束,Context控制它们何时该结束
缓冲大小设成 1 就等于无缓冲?性能差在哪
从 Go runtime 行为看,make(chan T, 1) 和 make(chan T) 都是同步模式:发送方仍需等待接收方就绪才能返回。区别只在“能否暂存一个值”,但这个“暂存”不缓解阻塞本质。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 缓冲为 1 仅比 0 多一次免阻塞发送机会,对 pipeline 吞吐提升几乎为零
- 真正起作用的是让缓冲 ≥ 2,比如
make(chan Result, 16),这样上游能连续发几条,下游处理间隙不至于立刻卡住 - 压测时重点看
runtime.ReadMemStats().Mallocs和 goroutine 数:缓冲过大会导致内存堆积,GC 压力上升;过小则 goroutine 频繁切换,goroutines数飙升
缓冲不是万能胶水,它只是给 pipeline 加了一小段“弹性管道”。真正决定吞吐的,是各 stage 的 CPU/IO 效率和上下游速率匹配度。这点容易被忽略——调了半天 buffer,结果瓶颈在某个正则表达式上。










