应控制goroutine并发数,用channel信号量或worker pool限流;闭包需传参防变量共享;错误管理优先用errgroup.Group统一处理。

用 goroutine 启动并行任务,但别直接裸奔
Go 里最常用的并行方式就是起 goroutine,但它不是“开越多越快”的银弹。比如批量调用 http 接口、处理一批文件、或并发查询数据库,你写 go doTask(item) 很容易,但没控制数量就可能打爆内存或触发限流。
实际做法是配合 sync.WaitGroup 等待全部完成,并用带缓冲的 channel 或 worker pool 控制并发数:
var wg sync.WaitGroup sem := make(chan struct{}, 10) // 最多 10 个并发 for _, item := range tasks { wg.Add(1) go func(t Task) { defer wg.Done() sem <- struct{}{}>
- 不加限制时,
goroutine 数量可能瞬间上千,GC 压力陡增 -
sem 这种 channel 信号量比 sync.Mutex 更轻量,适合纯计数场景 - 注意闭包捕获变量:必须把
item 作为参数传入匿名函数,否则所有 goroutine 可能共享最后一个值
用 errgroup.Group 统一管理错误和生命周期
原生 sync.WaitGroup 不支持错误传播,一旦某个任务出错,其他还在跑,你得自己设标志位或加锁判断。而 errgroup.Group(来自 golang.org/x/sync/errgroup)天然支持“任一出错即取消其余”。
适用场景:需要强一致性失败语义的任务链,比如批量写入 ES、同步多个下游服务:
立即学习“go语言免费学习笔记(深入)”;
g, ctx := errgroup.WithContext(context.Background()) for _, url := range urls { url := url // 防止闭包问题 g.Go(func() error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return nil }) } if err := g.Wait(); err != nil { log.Printf("at least one request failed: %v", err) }
-
WithContext返回的ctx会在任意子任务返回错误时被 cancel,可用于提前中断长耗时操作 - 它内部用了
sync.WaitGroup+sync.Once,线程安全,无需额外加锁 - 如果任务本身不接受
context.Context,记得在函数体内用select { case 主动响应取消
拆分任务时优先按数据边界而非固定数量切片
很多人习惯把一个大 slice 按每 100 个元素切一块,然后并发处理。但这在真实场景中常导致负载不均——比如某些分块里全是大文件、慢接口或重计算项,结果大部分 goroutine 早结束了,只剩一两个卡着。
更稳妥的做法是按“工作单元”本身拆分,而不是数组下标:
- 对文件处理:按文件粒度并发,而不是按字节范围切分(除非你在做 mmap 或流式解析)
- 对数据库查询:用
IN批量查不如按主键范围分页(如id BETWEEN ? AND ?),避免单条 SQL 过长或命中率低 - 对 API 调用:若后端支持批量接口(如
/batch?ids=a,b,c),优先用批量,而不是把单 ID 请求并发化
简单粗暴的等长切片只适合各单元耗时方差极小的场景,比如纯内存 JSON 解析。
runtime.GOMAXPROCS 一般不用手动调,但要注意 CGO 场景
默认情况下,Go 运行时会把 GOMAXPROCS 设为 CPU 核心数,这对绝大多数纯 Go 并发任务已足够。强行设高不会提升吞吐,反而增加调度开销。
唯一需要关注它的场景是启用了 CGO 且调用大量阻塞式 C 函数(如某些加密库、旧版 SQLite 驱动):
- 每个阻塞的 CGO 调用会占用一个 OS 线程,且该线程无法被 Go 调度器复用
- 此时若
GOMAXPROCS太小,新 goroutine 可能因无可用 P 而饿死 - 解决方案不是狂拉
GOMAXPROCS,而是改用非阻塞替代方案,或用runtime.LockOSThread()+ 单独线程池隔离 CGO
大多数现代 Go 项目根本碰不到这个问题,但一旦遇到“并发数上不去、CPU 却很低”的诡异现象,就得回头查是不是 CGO 在暗处拖后腿。