合并多个输入channel时必须显式关闭输出channel,否则主goroutine的for range会永久阻塞;正确做法是用sync.waitgroup等待所有发送者完成,再起goroutine调用close(out),且out需带缓冲(大小≥sender数)以避免死锁。

合并多个输入channel时必须显式关闭,不能靠“等完就完”
常见错误是启动多个goroutine往同一个out channel发数据,主协程for range out永远卡住——因为没人关它。Go里channel不会自动关闭,哪怕所有sender都退出了。
正确做法是用sync.WaitGroup计数所有sender,再起一个goroutine等完后close(out):
go func() {
wg.Wait()
close(out)
}()
注意:out必须带缓冲(大小 ≥ sender数),否则第一个sender可能因阻塞而无法完成,导致wg.Wait()永远不返回。
- 缓冲太小(如
make(chan int, 1)):多个sender争抢发送权,易死锁 - 缓冲太大(如
make(chan int, 10000)):掩盖背压,内存占用不可控 - 无缓冲channel:必须严格配对send/receive,不适合多源合并场景
分流到多个下游channel要避免竞态写,别直接共享map
分流不是“把数据扔进不同channel”,而是按规则路由。典型错误是让多个goroutine并发写同一个map[string]chan T,哪怕加了sync.RWMutex,高频写入也会成瓶颈。
更稳的方案是预分配+索引路由:
- 主线程初始化
outputs := make([]chan Item, n),每个channel独立创建 - 分流goroutine用
select配合default防阻塞,或用带缓冲channel缓解下游处理慢 - 若需按key分流(如user_id % 4),直接计算索引
outputs[hash%len(outputs)] ,不查map
如果真要动态key映射,用sync.Map只适合读远多于写的场景;聚合类分流(如按category统计)优先在主goroutine统一收口累加。
context.Done()必须贯穿每个stage,否则背压失效
一个stage卡住(比如HTTP请求没设timeout、数据库查询没传ctx),整条流就堵死。这不是设计缺陷,而是Go流水线的天然特性——channel阻塞会向上传导。
每个stage goroutine开头必须监听ctx.Done():
select {
case <p>关键点:</p>
-
http.Client必须配Timeout或用ctx控制,别用net/http.DefaultClient - DB操作(如
pgx.Conn.Query)必须传ctx,否则cancel信号无效 - 生成器stage也要检查
ctx.Err() != nil,避免无意义生产
缓冲大小不是调优终点,而是背压策略的体现
缓冲区不是越大越好,它本质是“允许积压多少数据”的策略声明。IO密集型stage(如API调用、文件读取)建议小缓冲(16~128),让上游稍等,防止下游雪崩;CPU密集型(如JSON解析、加密)可设大些(512~2048),减少调度开销。
最容易被忽略的是“缓冲链路失配”:上游buffer=1024,下游buffer=16,中间stage一卡,上游立刻OOM。解决方法:
- 各stage缓冲大小应接近,尤其IO/CPU混合流水线
- 用
select+default做非阻塞发送,失败时走重试或丢弃逻辑 - 监控
len(ch),超阈值触发告警或降级(如跳过校验)
真正的高性能不来自堆goroutine或狂开buffer,而来自每个stage对ctx、close、buffer三者的克制使用——它们共同构成可控的背压边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











