扇出时不能直接 for range channel,因为其会阻塞等待channel关闭,而生产者常不关闭channel,导致goroutine泄漏;正确做法是用select+done channel控制退出。

扇出(fan-out)时为什么不能直接 for range channel?
因为 for range 会阻塞等待 channel 关闭,而扇出阶段的生产者 goroutine 往往不会主动关 channel —— 它可能还在持续生成数据,或者根本不知道该由谁来关。一旦你用 for range 去读一个永远不关的 channel,整个 goroutine 就卡死在那,变成 goroutine 泄漏。
正确做法是:用 select + done channel 主动控制退出时机。常见错误写法:
for item := range inCh { // ❌ 卡死风险极高
outCh <p>应改为:</p><pre class="brush:php;toolbar:false;">for {
select {
case item, ok :=
-
ok判断能捕获 channel 关闭信号,避免 panic -
done是 context 或显式传入的取消 channel,确保可中断 - 两个退出路径都必须覆盖,缺一不可
扇入(fan-in)如何避免死锁和数据丢失?
扇入本质是把多个 channel 的数据合并到一个输出 channel,典型错误是:所有输入 channel 都没关,但主 goroutine 却在等某个特定 channel 先出数据,结果其他 channel 堵着没人读,最终全卡住。
关键约束有三个:
- 每个输入 channel 必须有独立的 goroutine 去读,不能共用一个循环去轮询多个 channel(否则会饿死某些 channel)
- 所有输入 channel 要么全部关闭,要么至少有一个能保证持续供数;若部分提前关闭,需用
select的default或超时分支兜底 - 输出 channel 如果带缓冲,缓冲大小要 ≥ 最大并发写入量,否则写操作会阻塞,反向卡住上游
示例中容易忽略的点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
cancel()
- 在主函数或 pipeline 入口处调用
context.WithCancel - 把
ctx透传给所有 goroutine(包括扇出 worker 和扇入 reader) - 只在主 goroutine 中根据业务逻辑决定是否调用
cancel(),比如超时、错误、用户中断 - 所有子 goroutine 通过
select监听ctx.Done(),收到信号后自行清理并退出
常见误用:
// ❌ 错误:每个 worker 都试图 cancel
go func(ctx context.Context) {
defer cancel() // 千万别这么干
...
}(ctx)
为什么带缓冲 channel 不等于“自动解决速率差”?
缓冲只是延缓了背压,不是消除背压。比如扇出 5 个 worker 往一个 chan int 写,缓冲设为 10,看似够用 —— 但如果消费者读得极慢,10 个槽位很快填满,所有 worker 会同时阻塞在 ch 上,整个 pipeline 停摆。
真正可控的速率调节靠三样东西:
-
sync.WaitGroup控制“启动多少个”,但不管“跑多快” -
semaphore(如make(chan struct{}, N))限制“同一时刻最多几个在跑”,这是真正的信号灯 -
context.WithTimeout或time.After给单次操作设上限,防止单个任务拖垮全局
缓冲 channel 更适合做短时抖动缓冲(比如瞬时 burst),而不是长期速率匹配。高吞吐场景下,优先考虑扇出数量 + 信号灯,而非盲目加大缓冲。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










