直接用golang.org/x/time/rate,它是Go生产限流的事实标准;rate.Limiter必须复用,不能每请求新建,否则限流失效;全局限流用包级变量,按IP或用户ID限流用sync.Map缓存并定期清理;burst不宜为0,Wait()与Allow()语义不同,分布式场景需权衡是否引入复杂方案。

直接用 golang.org/x/time/rate,别自己手写桶、别用 channel 模拟、别上第三方库——它就是 Go 生产限流的事实标准,精度高、零锁、线程安全,且已通过大规模验证。
rate.Limiter 实例不能每次请求都 new
限流失效最常见的原因是:在 HTTP handler 里写 rate.NewLimiter(10, 5)。每个请求拿到全新实例,桶永远是满的,等于没限。
- 全局限流(如全局 QPS ≤ 100):定义为包级变量,复用单个
*rate.Limiter - 按 IP 或用户 ID 限流:用
sync.Map缓存,key 是清洗后的 IP(优先取X-Forwarded-For最左字段,并校验可信代理跳数),value 是*rate.Limiter - 别直接用
r.RemoteAddr当 key——Nginx/ALB 后所有请求会落到同一个桶里 - burst 设为 0,第一次
Allow()就失败;设为 1,相当于强制匀速,扛不住双击或 CDN 预热;建议设为 QPS 的 2–3 倍
HTTP 中间件里该用 Allow() 还是 Wait()?
语义完全不同,混用等于放弃对延迟或成功率的控制。
-
limiter.Allow()立即返回bool,适合低延迟敏感接口(如搜索建议),失败就该立刻返回429 -
limiter.Wait(ctx)会阻塞直到拿到令牌,适合强保序场景(如支付回调),但必须传带超时的context,例如context.WithTimeout(r.Context(), 500*time.Millisecond),否则 goroutine 可能卡死 - 别在循环里反复调
Allow()等令牌——这是反模式;Wait(ctx)一行就搞定 - 需要预占多个令牌(比如按文件大小计费上传)?用
limiter.ReserveN(time.Now(), n),失败记得调reserve.Cancel()
如何动态管理 per-IP / per-path 的限流器?
硬编码每条路由的限流器不可维护,每次都 new 又浪费内存和 GC 压力。核心是「按需生成 + 惰性淘汰」。
- 键名建议用安全哈希,如
fmt.Sprintf("ip:%s", cleanIP),避免路径含参数导致键爆炸 - 用
sync.Map存储map[string]*rate.Limiter,读多写少下比RWMutex + map更轻量 - 不主动定时清理旧实例,改用「惰性淘汰」:每次访问时检查最后使用时间,超时(如 10 分钟)再丢弃
- burst 值要结合监控调优:过大会让限流形同虚设,过小则误伤率高
分布式部署下 rate.Limiter 为什么失效?
它纯内存实现,单机有效。3 个 Pod 部署时,用户发 30 QPS 就能绕过 10 QPS 限制——这不是 bug,而是明确的设计边界。
- 别急着上 Redis;先评估是否真需要分布式限流:很多业务用本地限流 + 自动扩缩容就能扛住峰值
- 若必须跨节点共享状态,核心是「原子操作 + 归一化 key」,推荐用 Redis + Lua 脚本,而不是改算法
- key 设计要归一化(统一小写、去端口、校验可信代理链),失败时可降级到本地
rate.Limiter,避免雪崩 - 真正难的不是写几行
rate.NewLimiter,而是决定谁该被限、按什么粒度限、burst怎么调、超时怎么设、拒绝响应头怎么填
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











