Go 中多生产者任务同步需用 sync.WaitGroup 配合 channel 关闭,因多生产者无法协调 close 时机,易 panic 或死锁;WaitGroup 确保主 goroutine 唯一、终局性关闭通道,消费者可安全 range;缓冲大小仅控制积压边界,不提升吞吐。

Go 语言中用 channel 实现任务同步,核心不在于“传数据”,而在于“控节奏”——它天然适合表达“等一个信号再继续”或“等所有人干完才收尾”这类逻辑。盲目套用 chan struct{} 或堆砌 select 往往反而引入死锁或漏信号,真正稳的方案几乎都绕不开 sync.WaitGroup 配合通道关闭。
为什么不能只靠 channel 关闭来判断所有 goroutine 完成
很多人误以为只要所有生产者都 close(ch),消费者用 for range ch 就能安全退出。但问题在于:close() 只能由发送方调用一次,多个生产者 goroutine 无法协调谁来关、何时关。
- 如果多个生产者都尝试
close(ch),第二次调用会 panic:"close of closed channel" - 如果让主 goroutine 等待所有生产者结束再关,又得靠额外同步机制(比如计数器),那不如直接用
sync.WaitGroup -
range只在通道关闭且缓冲/队列为空时退出;若某生产者卡住没发完、或已退出但数据还在缓冲区,range会提前或延迟退出
WaitGroup + channel 关闭才是多生产者同步的标准解法
正确模式是:主 goroutine 用 WaitGroup.Add(n) 声明要等 n 个生产者;每个生产者执行完后调用 wg.Done();主 goroutine 在 wg.Wait() 返回后,**唯一地**调用 close(ch)。
- 发送方(生产者)只负责发数据,不关 channel
- 接收方(消费者)只负责读,不关心谁发、发了多少
- 主 goroutine 扮演“协调者”角色,确保所有生产者退出后再关闭通道,避免竞争
- 消费者可放心使用
for result := range results,因为关闭动作是单点、原子、终局性的
示例关键片段:
results := make(chan Result, 100)
var wg sync.WaitGroup
<p>// 启动 5 个生产者
for i := 0; i </p><p>// 主 goroutine 等全部完成,再关通道
go func() {
wg.Wait()
close(results)
}()</p><p>// 消费者安全读取
for r := range results {
handle(r)
}
</p>
缓冲大小不是性能万能药,而是阻塞边界的显式声明
设 make(chan T, N) 的缓冲大小 N,本质是在说:“我允许最多 N 个未处理任务积压”。它不提升吞吐上限,只改变阻塞发生的时机和位置。
- N=0(无缓冲):每次
ch 都阻塞,直到有 goroutine 执行;适合强顺序、低延迟场景,如握手同步 - N=1:至少能缓存一个结果,避免生产者因消费者瞬时卡顿而停摆;常见于简单工作池
- N 过大(如 10000):内存占用上升,且可能掩盖消费者处理慢的问题,导致任务堆积、延迟不可控
- 真实瓶颈常在消费者侧(如 DB 写入、HTTP 调用),增大缓冲只是把压力从 channel 转移到下游,不解决根本问题
select + done channel 是取消与超时的标配,但别滥用 default
当任务需要响应中断或限时,select 结合 ctx.Done() 或 time.After() 是标准做法。但加 default 分支极易导致忙轮询或跳过关键信号。
- 写
select { case 会让doWork()不停执行,即使done已就绪 - 正确做法是去掉
default,让 goroutine 在无信号时挂起;或用time.AfterFunc单次触发 - 多个 channel 监听时,优先级由 case 顺序决定(随机选择就绪 case),不要依赖书写顺序做逻辑控制
- 若需非阻塞探测通道是否就绪,用
select { case x := ,但仅限明确需要“试探”的场景
最易被忽略的一点:channel 的生命周期管理必须和 goroutine 的启动/退出严格对齐。一个没被消费的 channel,或一个永远不退出的生产者 goroutine,都会导致整个程序无法终止——这种泄漏不会报错,只会让程序“看起来还活着”,实则卡在某个 上不动了。











