应优先用Wait()而非Allow(),因Wait()阻塞等待令牌并尊重context超时,避免瞬时超限;Allow()仅瞬时判断易致毛刺式429,且无法透出剩余令牌数与等待时间,运维排查困难。

HTTP 中间件里用 Allow() 而不是 Wait()
限流逻辑写在 HTTP handler 里,必须立刻响应,不能卡住请求。用 Wait() 会阻塞 goroutine 直到拿到令牌或超时,高并发下容易堆积等待、拖慢整体吞吐。正确做法是调 Allow() 立即判断:if !limiter.Allow() { http.Error(w, "too many requests", http.StatusTooManyRequests); return }。
常见错误是把 Wait() 放进中间件,结果一个慢请求拖垮整条链路;更隐蔽的问题是传了没设超时的 context.Background(),导致请求 hang 死。
-
Allow()平均耗时仅 10–50 ns,无锁、无 goroutine 开销 - 它不阻塞,也不排队,只做“此刻能不能过”的瞬时判断
- 适合绝大多数 Web 接口,尤其是需要低延迟响应的场景
按 IP 限流必须为每个 IP 创建独立 *rate.Limiter 实例
共用一个全局 *rate.Limiter 实例等于把所有用户塞进同一个桶——A 用户刷接口,B 用户直接被拦。真实 IP 限流必须按 key(如标准化后的 IP)隔离实例。
关键点不在“存”,而在“怎么取”和“怎么清理”:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Map[string]*rate.Limiter缓存,key 推荐用net.ParseIP(ipStr).To16().String(),避免 IPv4/IPv6 格式差异导致重复建桶 - 别用
r.RemoteAddr当 key —— 反向代理(Nginx/ALB/Cloudflare)会让所有请求显示同一地址;得先校验可信代理网段,再从X-Forwarded-For提取最左非私有 IP - 长期运行不清理会导致内存持续上涨;建议每分钟扫描一次,删掉
time.Since(lastAccess) > 30 * time.Minute的条目
rate.NewLimiter 的两个参数不是“QPS”和“并发数”
rate.NewLimiter(rate.Limit, burst) 的第一个参数是令牌生成速率(单位:token/秒),第二个是桶容量(最大可存多少 token)。它不是“每秒放行 N 次”和“最多同时处理 M 个请求”的简单映射。
典型配错现象:
-
rate.NewLimiter(10, 1)→ 第一秒只放过 1 次,其余全拒;burst 太小,突发流量直接打满即拦 -
rate.NewLimiter(0.005, 1)→ 小于 0.01 的rate.Limit因纳秒精度丢失,行为不可靠;想表达“每 200ms 一次”,直接写rate.Limit(5) - burst
推荐配置:QPS 为 N 时,burst 设为 N × 2 或 N × 3,容忍短时抖动或重试请求。
动态调速必须同时调 SetLimit() 和 SetBurst()
*rate.Limiter 支持运行时修改速率,但很多人只调 SetLimit(),忘了同步更新 SetBurst()。结果旧 burst 值卡住新 limit,限流效果严重偏离预期。
例如:原配置 rate.NewLimiter(5, 10),想升到 20 QPS。若只执行 limiter.SetLimit(20),桶还是最多存 10 个 token,实际峰值仍被压在 10 左右。
正确做法:
- 必须成对调用:
limiter.SetLimit(newLimit)+limiter.SetBurst(newBurst) - newBurst 至少 ≥ newLimit,否则依然达不到目标速率
- 注意:这两个方法是线程安全的,但调用时机需与业务节奏对齐(比如配合配置热更新信号)
真正难的不是写对一行 Allow(),而是让每个 key 对应的 limiter 实例既不泄漏、也不误判、还不被代理头骗过去——这些细节不处理,压测时流量就全涌进来。


















