
本文详解 Go 中 fanIn 模式下因未及时关闭输出通道导致的死锁问题,介绍使用 sync.WaitGroup 协调多 goroutine 完成后关闭通道的惯用方法,并提供可运行的修复代码与关键注意事项。
本文详解 go 中 `fanin` 模式下因未及时关闭输出通道导致的死锁问题,介绍使用 `sync.waitgroup` 协调多 goroutine 完成后关闭通道的惯用方法,并提供可运行的修复代码与关键注意事项。
在 Go 并发编程中,fanIn(扇入)是一种常见模式:将多个输入通道的数据合并到单个输出通道中,供下游消费。但若输出通道未被正确关闭,for range 循环将永远阻塞——这正是原代码发生死锁的根本原因:fanIn 返回的 out 通道从未关闭,而 main 函数在 for v := range stuff 处无限等待。
原实现存在两个关键问题:
- fanIn 中未关闭 out 通道:每个子 goroutine 从输入通道读取完毕后自然退出,但主逻辑无法感知“所有输入已耗尽”,因而无法触发 close(out);
- 嵌套 goroutine 引发竞态与资源浪费:fanIn 内部对每个 val 都启动新 goroutine 发送至 out(go func(c int) { out <- c }(val)),不仅无必要,还可能因缓冲区不足或调度延迟加剧阻塞风险。
✅ 正确解法是引入 sync.WaitGroup 跟踪所有读取 goroutine 的生命周期,并在全部完成后再关闭输出通道:
import "sync"
func fanIn(in ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int, 10) // 缓冲通道避免发送阻塞
// 启动每个输入通道的读取 goroutine
for _, ch := range in {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for val := range ch { // 自动在输入通道关闭时退出
out <- val // 直接发送,无需额外 goroutine
}
}(ch)
}
// 启动协程等待所有读取完成,然后关闭输出通道
go func() {
wg.Wait()
close(out)
}()
return out
}? 关键要点说明:
- wg.Add(1) 必须在 go 语句之前调用,确保计数器在 goroutine 启动前已更新;
- defer wg.Done() 放在 goroutine 内部,保证无论正常退出或 panic 都能减少计数;
- wg.Wait() 必须在独立 goroutine 中执行,否则会阻塞 fanIn 返回,导致调用方无法获取 out 通道;
- 移除冗余的嵌套 goroutine(即 go func(c int) { out <- c }(val)),直接 out <- val 更高效且避免 goroutine 泄漏;
- 输出通道需设为带缓冲(如 make(chan int, 10)),防止写入时因消费者未及时读取而阻塞读取 goroutine。
? 进阶建议:对于更复杂的场景(如需处理超时、取消或错误传播),可结合 context.Context 增强健壮性;若输入通道数量极大,还需考虑 wg 计数上限及 goroutine 创建开销。但就本例而言,sync.WaitGroup + close 是最简洁、符合 Go idioms 的解决方案。


















