
本文介绍如何在 go 中优雅地实现并发 api 请求的速率限制,解决直接在 goroutine 中调用阻塞式节流方法导致的竞争与失效问题,推荐使用 time.ticker 配合通道同步实现线程安全、高精度的全局限流。
在 Go 中对并发 HTTP 请求进行速率限制(如“20 次/10 秒”即平均 500ms/次)时,若在每个 goroutine 内部独立调用基于时间戳比较的 Throttle() 方法(如修改共享字段 LastCallTime),会因竞态条件(race condition)导致限流失效:多个 goroutine 可能几乎同时读取到相同的 LastCallTime,从而跳过等待,造成突发流量超出配额。
更可靠、简洁且符合 Go 并发哲学的方式是将限流逻辑从“每个请求自查”升级为“统一调度”——使用 time.Ticker 创建一个固定频率的令牌发放器,所有请求必须从该通道“领取令牌”后才能执行。这天然保证了全局速率上限,且无需锁或原子操作。
以下是一个生产就绪的示例:
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
// RateLimiter 封装限流器,支持并发安全调用
type RateLimiter struct {
ticker <-chan time.Time
}
func NewRateLimiter(interval time.Duration) *RateLimiter {
return &RateLimiter{
ticker: time.Tick(interval),
}
}
// Acquire 阻塞直到获得一个令牌(即到达下一个允许执行的时间点)
func (rl *RateLimiter) Acquire() {
<-rl.ticker
}
func main() {
// 例如:20 次/10 秒 → 平均间隔 500ms
limiter := NewRateLimiter(500 * time.Millisecond)
var wg sync.WaitGroup
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
// ... 更多 URL
}
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
// ✅ 关键:统一在请求前获取令牌
limiter.Acquire()
resp, err := http.Get(u)
if err != nil {
fmt.Printf("Request %d failed: %v\n", idx, err)
return
}
defer resp.Body.Close()
fmt.Printf("Request %d succeeded: %s\n", idx, resp.Status)
}(i, url)
}
wg.Wait()
}⚠️ 注意事项:
立即学习“go语言免费学习笔记(深入)”;
- time.Tick 是轻量级且线程安全的,适合长期运行的限流场景;
- 不要使用 time.After 替代,因其每次调用创建新定时器,资源开销大且无法复用;
- 若需动态调整速率(如熔断降级),应改用 time.NewTicker 并手动 Stop()/Reset();
- 对于更复杂的限流策略(如滑动窗口、令牌桶),建议引入成熟库如 golang.org/x/time/rate —— 它提供 rate.Limiter,支持 WaitN、AllowN 等高级接口,并内置上下文取消支持。
总结:限流的本质是协调并发行为,而非单个请求的自我约束。拥抱 Go 的 CSP 模型(通过 channel 同步),用 time.Ticker 实现声明式限流,代码更清晰、行为更确定、维护成本更低。










