fan-out 是将任务拆分为多个子任务并发执行,而非简单启动大量 goroutine;误用会导致泄漏、资源耗尽或结果丢失,需配合 waitgroup、context 或限流机制。

什么是 Fan-Out?为什么它常被误用为“开一堆 goroutine”
Fan-Out 的本质是把一个任务拆成多个子任务,并发执行,不是简单地 go f() 一堆。误用会导致 goroutine 泄漏、资源耗尽或结果丢失。
典型错误场景:遍历切片时对每个元素起一个 goroutine,但没控制并发数,也没等它们结束:
for _, item := range items {
go process(item) // ❌ 没有 sync.WaitGroup / context / channel 控制
}- 必须配对使用
sync.WaitGroup或context.WithCancel来确保可取消和可等待 - 高负载下建议加限流,比如用带缓冲的 channel 做 worker pool,而不是无节制 spawn
- 如果
process()可能 panic,不 recover 就会静默杀死 goroutine,结果无法收集
Fan-In 怎么写才不会丢数据或死锁
Fan-In 是把多个 goroutine 的输出合并到一个 channel,核心难点是“怎么知道所有输入都结束了”。常见死锁源于关闭时机不对或漏读。
最稳妥的做法是用 sync.WaitGroup + close() 组合,而不是靠 “发送完就 close”:
立即学习“go语言免费学习笔记(深入)”;
out := make(chan Result)
var wg sync.WaitGroup
wg.Add(len(inputs))
for _, input := range inputs {
go func(i Input) {
defer wg.Done()
out <- doWork(i)
}(input)
}
go func() {
wg.Wait()
close(out) // ✅ 关闭时机可控
}()
// 后续 range out 即可- 绝不能在每个 goroutine 里
close(out)—— 多次 close panic - 如果某个 goroutine 因 error 提前退出且没发值,
wg.Done()仍要调用,否则wg.Wait()卡住 - 若用
select配context.Done(),记得在 defer 里wg.Done(),否则 cancel 后 wg 无法完成
用 reflect.Select 做动态 Fan-In?别,真没必要
有人想用 reflect.Select 动态聚合不确定数量的 channel,实际几乎全是过度设计。Go 标准库的 sync/errgroup 或手动 WaitGroup 更清晰、更易测、更少出错。
-
reflect.Select无法类型安全,运行时报错难定位,调试成本远高于收益 - 它不解决关闭问题 —— 你依然得自己管理哪些 channel 该关、何时关
- 绝大多数真实场景(如 HTTP 批量请求、日志聚合)channel 数量固定或有上限,硬编码或池化更稳
Context 取消和超时怎么嵌进 Fan-In/Fan-Out 流程
不是在最外层加 ctx, cancel := context.WithTimeout(...) 就完事。每个 goroutine 必须主动监听 ctx.Done(),否则 cancel 不生效,goroutine 照样挂着。
- 所有阻塞操作(
http.Do、time.Sleep、chan recv)都要配合select+ctx.Done() - worker pool 模式下,worker goroutine 本身要检查
ctx.Done(),不能只让任务函数检查 - fan-in 的合并 goroutine 也要监听
ctx.Done(),避免主流程 cancel 后还在等未关闭的 out channel
复杂点在于:cancel 可能发生在任意中间环节,而 channel 的发送端和接收端必须都做好“提前退出+清理”的准备。没人替你关 channel,也没人替你 recover panic —— 这些都得自己写清楚。










