直接用 go 启动大量 goroutine 易崩因资源失控:内存暴涨、调度压力大、下游限流被击穿;须用信号量控制并发数,并配合 errgroup 统一错误处理与等待。

为什么直接用 go 启动大量 goroutine 容易崩
不是语法错,是资源失控。每个 goroutine 默认栈约 2KB,上万并发时内存暴涨;更关键的是调度器压力、上下文切换开销、以及目标服务(如数据库、HTTP API)的连接数/限流阈值被瞬间打穿。常见现象包括:runtime: out of memory、context deadline exceeded、下游返回 429 Too Many Requests。
真正要控制的不是“启动多少 goroutine”,而是“同一时刻有多少任务在执行”。所以必须引入并发限制层,而不是靠 time.Sleep 或随意加 sync.WaitGroup 就完事。
用 semaphore 控制并发数最直接
Go 标准库没有内置信号量,但用 chan struct{} 实现轻量级计数信号量极简单,且语义清晰:
func NewSemaphore(n int) chan struct{} {
return make(chan struct{}, n)
}
func (s chan struct{}) Acquire() {
s <- struct{}{}
}
func (s chan struct{}) Release() {
<-s
}
使用时包裹任务逻辑:
立即学习“go语言免费学习笔记(深入)”;
- 初始化信号量:
sem := NewSemaphore(10)表示最多 10 个任务并发 - 每个任务开始前调用
sem.Acquire(),阻塞直到有空位 - 任务结束(无论成功失败)必须调用
sem.Release(),建议用defer - 别把信号量传进 goroutine 再关 channel——它要复用,不能 close
配合 errgroup.Group 统一收口错误和等待
errgroup 不仅能等所有 goroutine 结束,还能自动传播第一个非 nil 错误,比裸写 sync.WaitGroup + 全局 error 变量更安全:
g, ctx := errgroup.WithContext(ctx) sem := NewSemaphore(10)for _, task := range tasks { task := task // 避免循环变量捕获问题 g.Go(func() error { sem.Acquire() defer sem.Release()
select { case <-ctx.Done(): return ctx.Err() default: } return doTask(task) // 实际业务逻辑 })}
if err := g.Wait(); err != nil { // 处理第一个出错的任务 }
注意点:
-
errgroup的Go方法会继承父ctx,任务中用select检查取消更可靠 - 不要在
Go函数里再起 goroutine 并试图用同一个errgroup—— 它不递归管理子 goroutine - 如果需要收集全部错误(而非只第一个),得自己用
sync.Mutex+ 切片存错
批量结果怎么有序返回?别依赖 goroutine 启动顺序
goroutine 执行完成时间不确定,按原始索引返回结果最稳妥。常见做法是让每个任务把自己处理的下标带上:
type Result struct {
Index int
Data interface{}
Err error
}
results := make([]Result, len(tasks))
g, _ := errgroup.WithContext(ctx)
sem := NewSemaphore(10)
for i, task := range tasks {
i, task := i, task
g.Go(func() error {
sem.Acquire()
defer sem.Release()
data, err := doTask(task)
results[i] = Result{Index: i, Data: data, Err: err}
return err
})}
g.Wait()
// 此时 results 已按原顺序填充,可直接用
容易踩的坑:
- 直接往共享切片
append会导致数据竞争,必须预分配 + 按索引赋值 - 如果任务本身有重试逻辑,记得在重试时也保持
i不变 - 结果结构体里存
Err而不只是打印日志,方便后续统一判断成败
实际并发批处理的复杂点不在启动,而在边界控制:信号量释放是否遗漏、上下文取消是否穿透到 IO 层、错误是否掩盖了超时或连接失败。这些地方松一扣,压测时就掉链子。










