根本原因是未控制并发数且缺乏同步机制:启动过多goroutine后主goroutine提前退出,加之for range闭包捕获循环变量地址导致数据错乱;须用sync.waitgroup显式计数、传值避免引用,并通过带缓冲channel限流。

goroutine分批执行时,为什么任务数一多就panic: all goroutines are asleep
根本原因是没控制并发数,启动了成百上千个goroutine后又没用sync.WaitGroup或channel同步等待,主goroutine提前退出。更隐蔽的是:用for range启动goroutine时,闭包捕获的是循环变量的地址,所有goroutine看到的都是最后一次迭代的值。
- 必须用
sync.WaitGroup显式计数,wg.Add()要在goroutine外调用 - 循环中传参别直接用
i,改用val := i; go fn(val)或go fn(i)(函数参数是值拷贝) - 批量任务建议每批10–100个goroutine,具体看任务I/O占比和系统负载,不是越多越好
用channel做并发控制器,限制同时运行的goroutine数量
比无脑go f()安全得多——用带缓冲的channel当“许可证”,每次执行前拿许可,执行完<code>sem 归还。这样不管总任务多少,真正并发的永远是缓冲区大小。
sem := make(chan struct{}, 10) // 最多10个并发
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
sem
- 缓冲大小设为
runtime.NumCPU()常是合理起点,但I/O密集型可放大到50–200 - 别在
defer里放,万一<code>doWorkpanic会导致死锁;用defer func(){确保执行 - 如果任务本身要返回结果,把
sem逻辑封装进工作函数,别裸露在启动代码里
用errgroup.Group替代手写WaitGroup+channel
errgroup.Group(来自golang.org/x/sync/errgroup)自动集成错误传播和并发限制,比原生组合更简洁。它底层就是sync.WaitGroup加channel,但帮你省了样板代码。
g, _ := errgroup.WithContext(ctx)
g.SetLimit(8) // 并发上限
for _, task := range tasks {
task := task // 防止闭包问题
g.Go(func() error {
return doWorkWithResult(task)
})
}
if err := g.Wait(); err != nil {
log.Printf("some task failed: %v", err)
}
-
SetLimit(0)表示不限并发,等价于普通Go();设为正整数才启用限流 - 只要任意一个goroutine返回非nil error,
g.Wait()就立刻返回该错误,适合“任一失败即终止”场景 - 如果需要收集全部错误,得自己用
sync.Mutex保护错误切片,errgroup不提供这个能力
分批执行的边界处理:任务总数不是batchSize整数倍怎么办
常见写法for i := 0; i 没问题,但容易漏掉最后一组少于<code>batchSize的任务。更稳妥的是用min计算右边界,避免切片越界。
batchSize := 20
for i := 0; i len(tasks) {
end = len(tasks)
}
batch := tasks[i:end]
// 启动这一批
}
- 别用
tasks[i:i+batchSize]硬切,Go runtime会panic “slice bounds out of range” - 如果每批要等全部完成再启下一批(串行分批),就不用
WaitGroup,直接for range batch { go ... }; wg.Wait() - 分批逻辑和并发控制是正交的:可以每批内部限流,也可以整批作为一个goroutine提交——看吞吐和响应延迟哪个优先
实际跑起来你会发现,真正卡住的往往不是goroutine语法,而是任务函数里的阻塞操作、未关闭的HTTP连接、或者忘了recover panic。分批只是调度策略,底下的单个任务是否健壮,才是压测时崩不崩的关键。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











