推荐用 semaphore.NewWeighted 控制最大并行数:需传 int64 类型权重,Acquire/Release 必须成对且检查 error,支持 context 取消与 panic 安全;兼容旧版可用 chan struct{} 模拟,但需注意容量计算与配对释放。

用 semaphore.NewWeighted 控制最大并行数(Go 1.21+)
Go 1.21 起标准库自带 sync.Semaphore,推荐直接用 semaphore.NewWeighted 初始化信号量。它比手写 channel 更安全,天然支持 context 取消、panic 安全释放、超时等待。
- 初始化必须传
int64:比如限 8 并发,得写semaphore.NewWeighted(int64(8)),传8会编译报错 -
Acquire(ctx, 1)必须在 goroutine 内部调用,且必须检查返回 error;若 ctx 已 cancel 或超时,直接跳过任务,别硬塞 -
Release(1)必须用defer放在函数最外层,否则 panic 后许可不归还,后续所有任务永久阻塞 - 权重不一定要是 1:大任务可占 3 单位,小任务占 1 单位,但需业务层自行定义语义,
Acquire和Release的 weight 值必须严格一致
用 chan struct{} 模拟信号量(兼容所有 Go 版本)
不需要引入外部包时,make(chan struct{}, N) 是最轻量、最 Go 风格的方案。它的本质是用 channel 缓冲区当“许可证池”,发送即占位,接收即释放。
- 别用
len(sem)判断是否还能获取——它返回已占用数,不是空闲数;可用cap(sem) - len(sem)算剩余容量,但一般没必要 - 获取和释放必须成对:写成
sem 和 <code><-sem,且后者必须包裹在defer func() { <-sem }()中,否则 panic 时会漏执行 - 千万别
close(sem):关闭后继续往里 send 会 panic,继续从里 recv 会立即返回零值,破坏限流逻辑 - 不要用
select { case sem 做非阻塞尝试——除非你明确要快速失败并丢弃任务
errgroup.Group.SetLimit 适合批量任务统一管理
如果你的任务要返回 error、需响应 context 取消、还要等全部完成,errgroup.Group 比裸写信号量更省心。它的 SetLimit 方法底层就是封装了信号量,但自动处理 panic 捕获、错误聚合、上下文传播。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 仅 Go 1.21+ 支持
SetLimit;旧版本得自己 wrap 或换x/sync/errgroup - 限流启用后,所有
g.Go()调用都会受控,但必须搭配context.WithTimeout使用,否则超时任务无法中断正在运行的 goroutine -
g.Wait()返回第一个非 nil error,其余 goroutine 仍会继续跑——若需强一致性中断,业务逻辑里得主动检查ctx.Err() - 别在
g.Go的函数里调用log.Fatal:它会 kill 整个进程,绕过 errgroup 的错误收集和 cleanup
HTTP 请求并发限速的关键位置在哪
限速必须落在真正发起网络请求的那一刻,而不是 goroutine 启动时、循环体外或 transport 配置里。很多人把信号量放错位置,结果看似限了,实际并发连接数还是爆表。
立即学习“go语言免费学习笔记(深入)”;
- 正确位置:
client.Do()前、http.NewRequest()后;哪怕请求失败或 timeout,也要确保Release执行 - 错误做法:把
sem.Acquire放在 for 循环里——那只是限制了 goroutine 创建速度,不是并发请求数 - 别混淆
http.Transport.MaxIdleConnsPerHost:它只控制空闲连接复用数量,和当前并发请求数无关 - 别用
runtime.GOMAXPROCS:它调的是 OS 线程数(M),不是 goroutine 并发数,完全不相关
真正的难点不在“怎么写”,而在“什么时候释放”和“谁负责释放”。信号量一旦漏释放,整个程序就卡死在那,日志里往往只看到最后几个任务没返回,找不到根因。所以无论用哪种方式,defer Release 这一行,必须出现在每个任务函数的第一行。

















