golang.org/x/time/rate 是唯一推荐的限流器实现,因其正确处理并发、时钟精度、预热及令牌桶语义;自行实现易导致令牌堆积、漂移、多扣或启动即击穿。

golang.org/x/time/rate 是唯一该用的限流器实现,别自己写、别用 channel 模拟令牌桶、也别拿 sync.Map + 计数器硬凑——生产环境踩过的坑已经足够多。
为什么不能自己实现令牌桶
自己手撸 rate.Limiter 看似可控,实则极易出错:
• 并发下用 time.AfterFunc 填充令牌,会因 GC 或调度延迟导致令牌堆积或骤空
• 用 atomic.LoadInt64 + 时间差计算剩余令牌,纳秒级精度难保,时钟漂移直接让限流失效
• 忘记加 atomic.CompareAndSwap 保护扣减逻辑,出现多扣或漏判(比如两个 goroutine 同时看到还有 1 个令牌,结果都取走了)
• 最致命的是:没有预热支持,服务刚启动就遭遇突发流量,桶是空的,瞬间打穿下游
rate.NewLimiter 两个参数到底怎么配
构造函数 rate.NewLimiter(rate.Limit, int) 的两个参数不是“QPS”和“并发数”,而是:
• 第一个参数 rate.Limit:每秒填充速率(float64),例如 rate.Every(200 * time.Millisecond) 等价于 5.0
• 第二个参数 burst:令牌桶最大容量,即允许的瞬时并发上限
- 设为
1就退化成严格匀速,毫无缓冲能力 -
rate.NewLimiter(10, 1)不是“每秒放行 10 次”,而是“桶最多存 1 个令牌,每秒补 10 个”——前 1 次立刻通过,后 9 次得等约 100ms 才能攒够 - 生产建议:
burst ≥ rate × 2,留出应对 GC STW 或网络抖动的余量 -
burst必须 ≥ 1;设为 0 会导致Allow()永远返回false
HTTP handler 里该用 Allow 还是 Wait
别无脑调 limiter.Allow() 然后返回 429:
• Allow() 只做快照判断,不阻塞、不等待,对突发毛刺完全无效
• Wait(ctx) 才是真实服务中的主力,它会阻塞直到拿到令牌或上下文超时
- 必须传入请求本身的
r.Context(),不能用context.Background(),否则客户端超时了,服务还在等令牌 - 错误处理只关心
context.DeadlineExceeded(正常超时);rate.ErrLimitExceeded只在AllowN等非阻塞方法中返回 - 示例:
if err := limiter.Wait(r.Context()); err != nil { http.Error(w, "too many requests", http.StatusTooManyRequests); return }
全局复用 vs 每请求新建限流器
限流器实例必须复用,这是最常被忽略的一点:
• rate.Limiter 内部维护原子计数器和 last 时间戳,每次 new 都等于重置桶,限流彻底失效
• 典型错误:func handler(w http.ResponseWriter, r *http.Request) { limiter := rate.NewLimiter(10, 5); ... }
- 单机统一限流:定义包级变量,初始化一次
- 按用户/IP/路径限流:用
sync.Map[string]*rate.Limiter,但 key 要短(如哈希后取前 16 字节),且必须定期清理 5 分钟未访问的 key - 切忌把
limiter声明在局部作用域,尤其是 HTTP handler 函数体内
真正难的不是写代码,是理解 burst 和 rate 的耦合关系——它们共同定义了“你允许多大的浪头打过来”,而这个浪头,永远比你压测时看到的峰值更 unpredictable。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











