直接使用 golang.org/x/time/rate,它已通过高并发、时钟漂移、上下文取消等真实压测验证;手写限流器易在原子性、burst 支持或时间精度上出错。

直接用 golang.org/x/time/rate,别自己手写——它已通过高并发、时钟漂移、上下文取消等真实压测验证;手写 channel 或计数器几乎必然在原子性、burst 支持或时间精度上翻车。
rate.NewLimiter 参数到底怎么设?
第一个参数是 rate.Limit(每秒补充令牌数,float64 类型),第二个是 burst(桶最大容量,int)。常见误解:rate.NewLimiter(10, 1) 不是“每秒放行 10 次”,而是“桶最多存 1 个令牌,每秒补 10 个”——第一秒只放过 1 次,其余全拒。
-
burst至少设成和速率相当,比如rate.NewLimiter(10, 10)才能支持突发流量 -
burst设太小(如 1)会导致连续请求频繁被拒;设太大(如 1000)等于形同虚设 - 速率低于
0.01(如0.005)会因纳秒级精度丢失导致行为异常
Wait 还是 Allow?HTTP 中间件该选哪个?
Wait 是 HTTP handler 的默认首选;Allow 只适合极低延迟、允许快速失败的场景(如健康检查探针)。
-
Wait(ctx)会阻塞直到拿到令牌或上下文超时/取消,天然匹配串行 handler 模型 - 务必传入带 deadline 的
context.Context,例如ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond),否则客户端断连后 goroutine 会卡死,连接池缓慢耗尽 -
Allow()只返回bool,没法透出剩余令牌数或等待时间;漏掉日志、没返回X-RateLimit-Remaining等标准头,运维完全无法排查
怎么支持一次扣多个令牌(上传/批量导出)?
只有 ReserveN 能扣多个,Allow 和 Wait 都固定为 1。
- 调用前必须缓存一次
now := time.Now(),后续所有ReserveN(now, n)都复用它;否则单请求内多次time.Now()会导致时间跳跃,限流行为飘忽 -
n必须 ≤burst容量,否则ReserveN直接返回不可用的*rate.Reservation - 拿到
res后,必须显式调res.Fulfill()(成功执行)或res.CancelAt(now)(放弃),否则令牌被预占却不扣除,桶迅速变空
按 IP 或用户限流,为什么不能共用一个 Limiter 实例?
*rate.Limiter 本身并发安全,但它不绑定任何标识——同一个实例无法区分用户 A 和用户 B 的请求。
- 用单个实例做全局 QPS 限流,恶意用户刷接口会把其他用户全堵死
- 需为每个 key(如 IP、userID)创建独立
*rate.Limiter,用sync.Map或带 TTL 的 LRU cache 管理生命周期 - 长期存活的 Limiter 实例不会自动 GC,需定期清理(比如 5 分钟无访问就删),否则内存泄漏
最易被忽略的点:所有涉及时间的操作(AllowN、ReserveN、Wait)都依赖传入的时间戳是否一致;单请求内混用多个 time.Now(),尤其在容器或虚拟机里,时钟抖动会让限流结果完全不可预测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











