rate.Limiter不能直接限制并发数,它基于令牌桶控制单位时间调用次数;要控制瞬时并发数,应设填充速率为0、桶容量为最大并发数,如rate.NewLimiter(0, 5)。

用 rate.Limiter 控制并发请求数量
Go 标准库的 golang.org/x/time/rate 提供了轻量、线程安全的限速器,适合控制 HTTP 请求的**瞬时并发数**(即同时发出的请求数),而不是每秒请求数(QPS)。关键在于用 rate.NewLimiter 初始化一个令牌桶,并在每次发请求前调用 limiter.Wait 阻塞等待可用令牌。
常见错误是把 rate.Limit 设为 1 以为能串行化,但实际它只限制“填充速率”,真正控制并发的是桶容量(第二个参数):
limiter := rate.NewLimiter(rate.Every(time.Second), 5) // 每秒最多 5 个请求,但允许突发最多 5 个 // ❌ 错误:这样不能限制并发,只限制平均速率 // ✅ 正确:要控并发,应设填充速率为 0,桶容量即最大并发数 limiter := rate.NewLimiter(0, 5) // 瞬时最多 5 个请求,不自动补充令牌
- 桶容量(如
5)就是你允许的最大并发请求数,超出会阻塞 - 填充速率为
0表示令牌不自动恢复,适合严格并发控制;若需“带缓冲的节流”,可设非零值(如rate.Every(200 * time.Millisecond)) - 每个 goroutine 调用
limiter.Wait(ctx)时,若无令牌则挂起,直到有空位 —— 这天然形成排队效果
用带缓冲 channel 实现显式排队与超时丢弃
仅靠 rate.Limiter 无法优雅处理排队超时或主动拒绝过载请求。这时需要一层显式队列,典型做法是用带缓冲的 chan struct{} 做信号槽,配合 select 实现非阻塞尝试或带超时的等待。
场景举例:你最多容忍 100 个请求排队,超时 3 秒还没轮到就直接返回错误:
queue := make(chan struct{}, 100)
// 启动一个消费者 goroutine 持续取任务
go func() {
for range queue {
// 实际发 HTTP 请求
resp, err := http.DefaultClient.Do(req)
// 处理 resp/err...
}
}()
<p>// 发起请求前尝试入队(带超时)
select {
case queue <- struct{}{}:
// 入队成功,后续由消费者执行
case <-time.After(3 * time.Second):
return errors.New("request timeout: queue full or slow")
}
- channel 缓冲区大小 = 最大排队长度,比纯
rate.Limiter更可控 - 必须搭配独立的消费者 goroutine,否则 channel 只是同步锁,起不到异步排队作用
- 注意:消费者出错(如 HTTP 超时)后必须从 channel 中取走信号,否则队列会被卡死
组合使用:限速 + 排队 + 上下文取消
真实服务中三者往往要叠加:既要防瞬时打爆下游,又要避免请求无限堆积,还要响应上游取消。推荐结构是「外层限速 → 中层排队 → 内层 HTTP 调用带 context」。
关键点在于各层 cancel 的传递和资源清理:
- 用
context.WithTimeout包裹整个流程,确保排队等待和 HTTP 请求都受同一 deadline 约束 -
limiter.Wait支持传入 context,若排队期间 context 被 cancel,它会立即返回context.Canceled - HTTP 请求必须用
http.NewRequestWithContext,否则context对Do无效 - 不要在多个 goroutine 中重复调用
limiter.Wait—— 它本身已并发安全,重复调用会导致逻辑混乱
典型错误是把 limiter.Wait 放在 channel 入队之后,导致排队成功但限速没生效;正确顺序是先过限速,再进队列。
为什么不用 semaphore 或自建 mutex?
有人倾向用 sync.Mutex 或第三方 semaphore 库手动计数,但这容易出问题:
-
sync.Mutex无法实现带超时的等待,也不能自然支持上下文取消 - 手写计数器在 panic、HTTP error、context cancel 等边界场景下极易漏释放,造成 goroutine 泄漏或永久阻塞
-
golang.org/x/time/rate.Limiter经过长期验证,内部用time.Timer和 channel 实现,无锁且精确 - 标准库
net/http的Transport.MaxConnsPerHost是连接层限速,和业务逻辑层的请求排队无关,不能替代
真正难的不是选哪个工具,而是厘清“并发控制”发生在哪一层:是连接池?是请求生成速率?还是下游服务能承受的瞬时压力?混用不同层级的限速反而会让行为更不可预测。


















