直接用 golang.org/x/time/rate 就够了,因其基于纳秒级精度的线程安全 Token Bucket,支持预热与突发控制,避免手写限流器时的原子性、竞态、时间漂移等常见错误。

为什么直接用 golang.org/x/time/rate 就够了
Go 标准库没有内置限流器,但官方扩展包 golang.org/x/time/rate 提供的 rate.Limiter 就是基于 Token Bucket 实现的,且线程安全、精度高(纳秒级)、支持预热和突发控制。自己手写容易在并发场景下漏掉 atomic 或锁竞争问题,比如误用非原子操作更新 token 数量,或在 Allow() 和 Reserve() 之间出现竞态。
常见错误现象:Allow() 返回 true 后实际被拒绝(因未考虑时间漂移),或高并发下调用 Wait() 阻塞异常久(因未设置 context.WithTimeout)。
- 使用场景:HTTP 中间件限流、RPC 接口调用频控、后台任务调度节流
-
rate.NewLimiter(rate.Limit(10), 5)表示「每秒最多 10 个请求,初始/最大突发 5 个」 - 注意:
burst必须 ≥ 1,且不能小于limit的倒数(否则桶无法填满)
如何在 HTTP 中间件中正确使用 rate.Limiter
别在 handler 里每次 new 一个 rate.Limiter——这会导致每个请求都重置桶,完全失效。必须复用同一个实例,按用户 ID、IP 或路径做 key 分桶时,也得用 sync.Map 或带 TTL 的内存缓存管理,避免无限增长。
实操建议:
- 全局共享一个
rate.Limiter用于服务级限流:var globalLimiter = rate.NewLimiter(100, 20) - 按 IP 限流时,用
sync.Map存储:ipLimiters.LoadOrStore(clientIP, rate.NewLimiter(5, 2)) - 务必用
limiter.Wait(ctx)而非Allow()+ 手动 sleep——前者会自动计算等待时间并响应取消信号 - 如果用
Allow(),记得检查返回值后立即执行业务逻辑,不要中间插入 IO 操作,否则窗口已过期
rate.Limiter 的三个关键参数怎么配才不翻车
limit 是每秒令牌生成速率(rate.Limit 类型),不是“每 N 秒几个”;burst 是桶容量,决定最大突发量;第三个隐含参数是起始时间,默认为第一次调用时的当前时间,但可通过 SetLimitAndBurst() 动态调整。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
典型陷阱:
- 设
rate.Limit(0.2)想实现“每 5 秒 1 次”,结果发现前 5 秒全放行——因为burst默认为 1,桶里初始就有 1 个 token,且令牌生成太慢,桶长期不满,Allow()总返回true - 把
burst设得过大(如 1000),导致短时流量洪峰打垮下游,而算法本身不会主动拒绝,只延时排队 - 调用
SetLimitAndBurst()时没加锁或没考虑并发调用,造成状态不一致(该方法不是原子的)
推荐做法:先固定 burst = limit * 2 作为起点,再根据压测中 P99 延迟和错误率反向调小。
需要自定义 Token Bucket 时,哪些地方必须用 atomic
仅当必须绕过 golang.org/x/time/rate(例如要集成分布式存储、或需定制令牌生成策略)才手写。核心变量 tokens、last(上次更新时间)必须用 atomic.Float64 和 atomic.Int64,且读写成对——比如 atomic.LoadInt64(&l.last) 后,必须用 atomic.CompareAndSwapInt64 更新,否则多 goroutine 下会覆盖彼此的时间戳,导致令牌超发。
容易忽略的点:
- 浮点运算不精确,
tokens累加时要用math.Round()截断,否则误差累积几小时后可能溢出 - 不处理系统时钟回拨(NTP 校正),会导致
last变小,误算大量 token 补发 - 没做负数保护:令牌数不能低于 0,但
atomic.LoadFloat64后直接减可能得到负值,需用math.Max(0, ...)修正
真要自研,优先考虑用 Redis + Lua 做分布式桶,而不是在 Go 里拼原子操作——复杂度和风险远高于收益。

















