最直接有效的方式是用带缓冲的 chan struct{} 模拟信号量,容量即最大并发数,每次请求前写入占位、结束后读出释放,硬性限制同时运行的 goroutine 数量。

Go 限制 HTTP 并发请求数,最直接有效的方式是用带缓冲的 chan struct{} 模拟信号量,它轻量、无依赖、线程安全,且能硬性卡住同时运行的 goroutine 数量——不是“尽量控制”,而是“绝不超限”。
用 chan struct{} 实现并发数硬限制
这是控制并发最常用也最可控的手法。本质是把 channel 当作“许可池”:容量即最大并发数,每次请求前写入一个空结构体占位,结束后读出释放。它不关心请求耗时长短,只保证同一时刻最多 N 个请求在跑。
- 适合场景:批量调用第三方 API、爬虫、数据同步等易触发连接耗尽或目标服务限流的场景
- 错误现象:不加限制时,
go client.Get(url)起几千 goroutine,很快出现dial tcp: lookup xxx: no such host或too many open files - 参数差异:
make(chan struct{}, 10)表示最多 10 并发;设为 1 就退化成串行,设太大则失去控制意义 - 必须配
defer func() { ,否则 panic 或提前 return 会导致许可永久丢失,channel 慢慢被占满,后续所有请求阻塞
var sem = make(chan struct{}, 20) // 最大并发20
func fetchURL(url string) error {
sem <- struct{}{} // 阻塞等待许可
defer func() { <-sem }() // 一定要放 defer,确保释放
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
return nil}
为什么不用 WaitGroup 单独控并发?
sync.WaitGroup 只负责“等全部完成”,它不阻止你起 1000 个 goroutine —— 这正是问题源头。它和并发控制无关,只是收尾工具。
- 常见误用:先
wg.Add(1000),再起 1000 个 goroutine,结果瞬间打满本地资源 - 正确配合方式:
WaitGroup必须和信号量(或 worker 池)一起用:先用 channel 控制“谁可以开干”,再用wg.Done()记录“谁干完了” - 性能影响:纯
WaitGroup零开销,但放任 goroutine 泛滥会引发调度器压力、内存暴涨、文件描述符耗尽
令牌桶 rate.Limiter 和信号量的区别与组合用法
rate.NewLimiter(10, 5) 控的是“每秒多少请求”,是速率维度;chan struct{} 控的是“同一时刻几个请求”,是数量维度。两者解决不同问题,常需共存。
- 单独用限流器不行:即使每秒只发 10 个请求,若每个请求耗时 5 秒,第 1 秒就起 10 个 goroutine,第 2 秒又起 10 个……瞬间堆积 50 个活跃请求
- 单独用信号量也不够:允许 20 并发,但若 20 个请求在 100ms 内全发出,下游服务可能因瞬时 QPS 过高而 429
- 推荐组合:先过信号量拿执行权,再用
limiter.Wait(ctx)等令牌——既防本地资源崩,也防压垮对方
真正容易被忽略的不是“怎么写”,而是“怎么清理”:长期运行的服务里,如果按 IP 或用户建了大量 rate.Limiter 实例却从不回收,map 会无限膨胀;而信号量 channel 虽简单,但一旦某次 defer 因 panic 未执行,那个 slot 就永远卡死。稳定性不在代码多炫,而在每一处释放是否牢靠。










