应使用 semaphore.weighted 控制并发,避免资源失控;acquire 与 defer release 必须成对;任务耗时差异大时需结合带缓冲 channel 和 worker pool 实现排队与复用。

直接用 go f() 启一批任务,大概率会崩——不是语法错,是资源失控:内存暴涨、下游 429、runtime: out of memory 或大量 context.DeadlineExceeded,但日志里找不到哪批任务失控。
用 semaphore.Weighted 控制最大并发数
别手写计数器或用 sync.Mutex 加锁;官方 golang.org/x/sync/semaphore 支持带 context 的获取与超时,更安全可靠。
-
sem := semaphore.NewWeighted(8)表示最多 8 个任务同时执行 - 每个 goroutine 必须在开始前调用
sem.Acquire(ctx, 1),失败则跳过或重试 -
defer sem.Release(1)必须成对出现,panic 时也得释放——所以defer是唯一合理位置 - 别把
Acquire放在 goroutine 外面,否则退化成串行 - 如果任务耗时差异大(比如有的 100ms,有的 5s),单靠信号量不够,还得加缓冲队列
用 chan Task + worker pool 实现排队与复用
信号量只管“放行”,不存任务。突发流量远超处理能力时,你需要一个缓冲区暂存请求,避免调用方阻塞或丢弃。
- 定义
type Task struct { ID string; Fn func() },输入 channel 建议带缓冲:jobs := make(chan Task, 100) - 启动固定数量 worker:
for i := 0; i - 提交任务走
select防止阻塞:select { case jobs - worker 内部必须检查
ctx.Done(),尤其是 HTTP、DB、IO 等阻塞操作 - 别忘了
close(jobs),否则for range jobs永远不会退出
用 errgroup.Group 统一处理错误与取消
sync.WaitGroup 只等完成,不管错误;errgroup.Group 天然支持“任一出错即取消其余”,且自动继承 context。
- 初始化:
g, ctx := errgroup.WithContext(context.WithTimeout(context.Background(), 30*time.Second)) - 提交任务:
g.Go(func() error { return process(ctx, task) }),不用手动wg.Add/Done - 等待:
if err := g.Wait(); err != nil,返回第一个非 nil 错误 - 关键细节:任务函数内部必须主动响应
ctx.Err(),比如用http.NewRequestWithContext(ctx, ...) - 别把
g.Go套在另一个go里——它不递归管理子 goroutine
结果收集要保序、防竞态、不丢错
goroutine 执行完成时间不确定,不能依赖执行顺序往 slice 里写;闭包捕获循环变量 i 是高频翻车点。
- 结果结构体必须含原始索引:
type Result struct { Index int; Data interface{}; Err error } - 结果 channel 要带缓冲:
results := make(chan Result, len(tasks)) - 每个 goroutine 结束后发一次:
results - 主 goroutine 循环接收
len(tasks)次,按Index填入结果切片 - 传参防闭包共享:
go func(idx int, task Task) { ... }(i, task),而不是go func() { ... }()
真正难的不是“怎么并发”,而是“谁该先跑、跑多久、失败了通知谁、超时了怎么收尾”。这些细节一旦漏掉一个,压测时就容易在凌晨三点收到告警。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











