golang.org/x/time/rate.Limiter 不是“每秒放行 N 个请求”,而是基于令牌桶匀速填充、允许突发的限流器;NewLimiter(10,10) 表示长期均值 10 QPS、最大突发 10,非固定窗口重置。

为什么 golang.org/x/time/rate 的 Limiter 不是“每秒放行 N 个请求”?
它默认按「匀速」分配令牌,不是按秒切片统计。比如 NewLimiter(10, 10) 表示:长期平均 10 QPS,但允许短时突发最多 10 个请求(burst=10),之后必须等令牌慢慢补满。如果你期望“严格每秒重置计数器”,它做不到——那是滑动窗口或固定窗口的逻辑,rate.Limiter 不提供。
常见错误现象:Allow() 或 Reserve() 在高并发下突然开始频繁返回 false,但你检查 QPS 并没超限——大概率是因为 burst 耗尽后,后续请求必须等待令牌生成间隔(比如 100ms 一个),而你误以为该“立刻重试”。
- 使用场景:适合保护下游稳定性(如 DB 连接池、API 调用频次),不适合做精确的“每秒配额扣减”类计费
-
burst值不能小于limit,否则构造会 panic;设得过大等于放弃限流效果 - 性能影响极小,
AllowN(time.Now(), n)是无锁原子操作,但要注意传入的时间必须是单调递增的(别用time.Now()在不同 goroutine 里反复调,建议复用同一时刻或用limiter.Now())
如何用 Reserve() 实现带等待的限流(比如 HTTP 中间件)
Reserve() 返回一个 *rate.Reservation,它告诉你“这个请求能不能过、如果能,要等多久”。比 Allow() 更灵活,尤其适合需要阻塞等待的场景。
常见错误现象:直接调 res.OK() == false 就拒绝请求,却忽略了 res.Delay() 可能是非零但可接受的——比如你允许最多等待 200ms,那就不该立刻 429。
立即学习“go语言免费学习笔记(深入)”;
- 实操建议:先
res := limiter.ReserveN(time.Now(), 1),再判断res.Delay() ,满足则 <code>time.Sleep(res.Delay())后放行 - 别在 defer 里调
res.Cancel()——它只应在确认不执行该请求时才调(比如中间件提前 return),否则可能干扰令牌计数 - HTTP 中间件中,务必用
ctx.Request.Context()做 cancel 控制,避免 Sleep 被客户端断连后还继续等
AllowN() 和 ReserveN() 参数时间戳写错会怎样?
两个函数都要求传入一个时间点,用于计算到此刻应有多少令牌。如果传了明显过时的时间(比如 5 秒前),AllowN() 可能误判为“还有大量余量”,导致超发;如果传了未来时间(比如 time.Now().Add(10 * time.Second)),则会强制认为“还没到能用的时候”,一律返回 false 或大 Delay。
最典型翻车现场:在 for 循环里反复用 time.Now() 获取时间,但没缓存,导致同一请求内多次调用时时间戳跳跃(尤其在虚拟机或容器里系统时钟抖动),结果限流行为飘忽不定。
- 正确做法:在请求入口统一取一次
now := time.Now(),所有AllowN(now, ...)/ReserveN(now, ...)都复用它 - 如果你用的是
golang.org/x/time/ratev0.15.0+,可以直接用limiter.Now(),它内部做了 monotonic clock 适配,比裸用time.Now()更稳 - 测试时别 mock
time.Now全局函数——改用依赖注入方式传入func() time.Time,否则单元测试容易因时序失败
多个 API 路径共用一个 rate.Limiter 安全吗?
安全,但不推荐。因为 rate.Limiter 是无状态的纯内存结构,goroutine-safe,多个路径并发调它的方法完全没问题。问题出在语义上:/login 和 /search 的流量特征、敏感度、burst 需求完全不同,混用一个限流器会导致要么某接口被误伤,要么另一接口形同虚设。
容易被忽略的地方:很多人图省事,在全局定义一个 var globalLimiter = rate.NewLimiter(...),然后所有 handler 都用它——上线后发现登录爆破攻击把搜索接口也拖慢了,却查不出原因。
- 实操建议:按业务维度拆,比如
loginLimiter、payLimiter、queryLimiter,每个独立配置limit和burst - 如果真要动态配额(比如按用户等级),不要改
Limiter的字段(它没提供 Set 方法),而是重建新实例 + 原子替换指针(用sync/atomic.Value) - 注意 GC:高频新建
Limiter实例本身开销不大,但若每秒建几百个,可能触发不必要的 GC 扫描——优先复用或预热初始化










