根本原因是未等待所有goroutine完成就关闭接收channel;需用sync.waitgroup或done channel同步,避免因超时、panic或写入无缓冲channel导致结果缺失。

Go 里用 fan-out 启动多个 goroutine,为什么结果总漏掉几个?
根本原因是没等所有 goroutine 完成就提前关闭了接收 channel。扇出(fan-out)本身只是并发启动,不自带同步语义。
- 必须用
sync.WaitGroup或额外的 done channel 控制 goroutine 生命周期,不能只靠range接收就认为“全跑完了” - 如果扇出的 goroutine 里有阻塞操作(比如 HTTP 请求、数据库查询),个别超时或 panic 会导致对应 goroutine 提前退出,而主流程若没捕获,就表现为结果缺失
- 别在扇出循环里直接往同一个
chan<t></t>写入——没有缓冲且无同步保护时会 panic;要么加缓冲(make(chan T, N)),要么用select配合default防写满阻塞
fan-in 聚合多个 channel,怎么避免死锁和 goroutine 泄漏?
扇入(fan-in)不是简单把几个 channel 往一个函数里一塞就行;核心矛盾是:谁关 channel、何时关、关了之后接收方怎么感知结束。
- 每个被扇入的 source channel 必须由其生产者显式关闭(通常是对应 goroutine 结束前调用
close(ch)),不能依赖外部“统一关” - 用
for range从 channel 读取天然支持关闭检测,但前提是该 channel 确实会被关闭;如果某个 source goroutine 永不退出(比如忘了处理错误退出条件),整个 fan-in 就卡住 - 推荐组合写法:
go func() { defer close(out); for v := range in { out —— 把关闭逻辑绑定在 goroutine 生命周期末尾,而不是放在主流程里判断
用 select + default 做非阻塞扇入,为什么反而吞掉数据?
这是对 select 的典型误用:default 分支让接收变成“尽力而为”,但 channel 缓冲区未满时写入成功,满时就直接跳过,数据彻底丢弃。
- 除非你明确接受丢失(比如日志采样),否则不要在 fan-in 聚合路径上用
select+default接收 - 真正需要非阻塞的场景,应改用带缓冲的 channel(容量设为预期峰值并发数),再配合
len(ch) == cap(ch)做主动丢弃判断,至少能 log 一下 - 更稳妥的做法是用
context.WithTimeout包裹单个 source goroutine,超时后主动 close 对应 channel,让 fan-in 的range自然退出该路
为什么 time.After 和 context.WithTimeout 在扇出中表现不同?
time.After 创建的是独立 timer,无法被取消;而 context.WithTimeout 返回的 ctx.Done() channel 可被上游主动 cancel,这对扇出后的资源清理至关重要。
立即学习“go语言免费学习笔记(深入)”;
- 在扇出 goroutine 中直接用
,即使主流程已放弃等待,这个 timer 仍会运行到底,goroutine 无法及时回收 - 正确做法是把
ctx传进每个扇出 goroutine,在关键阻塞点(如http.Client.Do、db.QueryRowContext)使用带 ctx 的版本,并在 select 中监听ctx.Done() - 注意:
ctx.Done()关闭后,再次读取会立即返回零值,所以要检查err == context.Canceled或err == context.DeadlineExceeded来区分正常结束和中断
扇入扇出看着是模式,实际每条 goroutine 的启停边界、channel 的生命周期、错误传播路径,都得手动对齐;最容易被忽略的是“谁负责关闭 channel”这件事——它既不能交给 GC,也不能指望别人代劳。










