常见原因是主协程未等待子协程结束便退出,而子协程向未缓冲或已关闭的通道写入;正确做法是用sync.waitgroup协调、带缓冲通道接收结果,并由独立goroutine调用wg.wait()后关闭通道。

goroutine扇出时为什么任务数一多就panic: all goroutines are asleep
常见原因是主协程没等子协程结束就退出了,而子协程又在向已关闭或未缓冲的chan写入。比如用无缓冲通道接收结果但没开足够goroutine读取,或者忘了用sync.WaitGroup或context.WithCancel做协调。
正确做法是:扇出前初始化sync.WaitGroup,每个goroutine启动前wg.Add(1),结束前wg.Done();结果通道建议用带缓冲的make(chan result, len(tasks)),避免阻塞。
- 别用
for range直接遍历未关闭的通道来“等待”,它会永远卡住 - 如果任务可能失败,别把错误处理全压到汇聚端,应在goroutine内捕获并发送
result{err: …} - 避免在goroutine里直接操作共享变量(如全局map),改用通道或
sync.Mutex
如何用channel+WaitGroup安全汇聚多个goroutine结果
汇聚不是简单收集,关键是保证“所有结果都拿到且不漏、不重复、不 panic”。典型结构是:一个结果通道 + WaitGroup + 单独goroutine负责关闭通道。
示例模式:
results := make(chan Result, len(tasks))
var wg sync.WaitGroup
<p>// 扇出
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
res := doWork(t)
results </p><p>// 单独goroutine等全部完成再关通道
go func() {
wg.Wait()
close(results)
}()</p><p>// 汇聚:range自动退出
for r := range results {
handle(r)
}</p>
- 必须用
go func() { wg.Wait(); close(ch) }(),不能在主goroutine里wg.Wait()后关通道——否则汇聚循环无法启动 - 通道缓冲大小设为
len(tasks)最稳妥;设0(无缓冲)则要求汇聚端必须先运行,实际很难控制 - 如果任务耗时差异大,汇聚端
range不会早退,它会等close(results)才结束
需要超时或可取消时,为什么不能只靠time.After
time.After只发一次信号,无法中断正在运行的goroutine。若某个doWork卡死,它仍会往results通道发结果,而汇聚端可能已因超时退出,导致写入已关闭通道 panic。
正确方式是把context.Context传进每个goroutine,并在工作函数中定期检查ctx.Done():
go func(t Task) {
defer wg.Done()
select {
case results
- 别在goroutine里直接
return而不发任何值——汇聚端会少收一个结果,range提前结束 -
ctx.WithTimeout比time.After更可靠,因为能传播取消信号到下游goroutine - 如果
doWork本身支持context(如http.Client.Do),优先用它原生的context参数,别自己轮询ctx.Done()
扇出任务量极大时,如何避免goroutine爆炸和内存溢出
一次性启动10万goroutine看似Go特色,但若每个都占2KB栈+额外堆对象,瞬间吃光内存。真实场景应限流。
两种轻量方案:
- 用带缓冲的
sem := make(chan struct{}, 100),每个goroutine启动前sem ,结束后<code>,控制并发数 - 用
worker pool模式:固定N个goroutine从任务通道取活干,扇出只负责投递taskCh ,避免数量失控
注意:runtime.GOMAXPROCS不控制goroutine数量,只影响OS线程调度;真正要管的是你主动创建的数量和资源占用。
扇出任务本身不重,但汇聚逻辑若含复杂计算或IO,也得考虑是否需要进一步拆解——比如结果通道后接另一个worker池做后续处理。这点容易被忽略,以为“扇出-汇聚”两步就完事了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











