扇出应通过 select + chan 实现可控并发调度与结果聚合,而非直接 go f();需用输入 channel 分发任务、n 个 worker 消费、输出 channel 收集结果,并用 select 处理超时、取消与错误。

Go 里用 select + chan 实现扇出,别直接 go f() 起一堆 goroutine
扇出不是“起 10 个 goroutine 就完事”,关键在可控的并发调度和结果聚合。直接 go worker(task) 十次,等于放弃错误传播、超时控制、取消信号和结果顺序——你只是把串行 bug 换成了并发 bug。
正确做法是:一个输入 channel 分发任务,N 个 worker 从它读,一个输出 channel 收结果。用 select 套 case 或 <code>ctx.Done() 来响应取消。
- worker 必须检查
ctx.Err()或接收关闭的inchannel,否则可能卡死 - 输入 channel 最好带缓冲(
make(chan Task, 100)),避免生产者被阻塞,尤其当 worker 暂时全忙时 - 输出 channel 不要缓冲——除非你明确需要背压;否则用无缓冲 +
range接收更安全
sync.WaitGroup 和 context.Context 必须一起用,光等 goroutine 结束不等于任务完成
常见错误:起一堆 worker,用 wg.Add(1) + wg.Done(),最后 wg.Wait() —— 这只保证 goroutine 退出,不保证它们干完了活,也不管中间是否 panic、超时或被取消。
真正要等的是“所有任务已处理完毕或被中止”。所以每个 worker 启动前应传入 ctx,并在循环里检查 ctx.Err() != nil;wg 只负责生命周期,ctx 负责语义终止。
立即学习“go语言免费学习笔记(深入)”;
- 别在 worker 内部忽略
ctx.Err(),比如做 HTTP 请求时没传ctx给http.Client.Do() -
wg.Wait()前必须确保所有wg.Add()已调用,推荐在启动 goroutine 前统一wg.Add(n),而不是在每个 goroutine 里调 - 如果 worker 会向输出 channel 发送结果,记得在
defer里关掉它,或用sync.Once防止多次关闭 panic
扇出后怎么收拢结果?别用 for i := 0; i 硬等 N 次
你不知道哪个 worker 先返回,也不知道会不会有 worker 失败不发结果。硬写 for i := 0; i 很危险:万一某个 worker panic 或卡住,主协程就永远卡在这儿。
应该用 range 配合 close(out),而 close(out) 的时机由最后一个 worker 控制——通常靠 wg.Wait() 后关闭。
- 输出 channel 必须由 producer(即 worker 集合)关闭,绝不能由 consumer 关闭
- 如果某些 worker 可能不发结果(比如任务被跳过),考虑加一个
resultCh+doneCh双 channel,或用errgroup.Group简化 - 想保留结果顺序?别依赖发送顺序。要么让每个 result 带
id字段,接收端再排序;要么用sync.Map缓存,但要注意并发写冲突
用 errgroup.Group 能省事,但别把它当黑盒
errgroup.Group 确实封装了 ctx 传播、错误短路、Wait() 等逻辑,但它不解决扇出结构本身的设计问题:比如输入 channel 容量、worker 如何优雅退出、结果如何去重或合并。
它只是帮你少写几行样板代码,不是自动修复竞态或资源泄漏的魔法。
- 传给
eg.Go()的函数如果内部没处理ctx,eg.Wait()仍会等到天荒地老 -
eg.Wait()返回第一个非nil错误,但你可能需要收集全部错误,这时得自己维护sync.Mutex+[]error - 别在
eg.Go()里启动子 goroutine 并忘记等它——errgroup只等你传进去的那个函数返回










