go 官方 rate.limiter 是无锁、高精度、低内存开销的生产级令牌桶实现,需正确使用 rate.every 和 burst 参数,全局复用实例,按维度隔离并用 sync.map 缓存带过期清理。

用 golang.org/x/time/rate 实现最简令牌桶限流
直接上手就能用,不需要引入任何第三方包。Go 官方的 rate.Limiter 是无锁、高精度、低内存开销的生产级实现,比自己手写或套用 uber-go/ratelimit 更稳。
关键点在于理解两个参数:rate.Every 控制令牌生成间隔(不是 QPS 数字本身),Burst 是桶容量(最大允许突发请求数)。比如每秒 10 次,应写成 rate.Every(time.Second / 10),而不是 rate.Every(time.Second) 然后靠 Burst 去“凑”。
-
Allow()是非阻塞判断,适合快速拒绝;ReserveN()可获取预留时间,适合做排队或延迟响应 - 不要在中间件里每次调用
rate.NewLimiter—— 它是轻量,但重复初始化没意义,全局复用一个实例即可 -
r.Burst()返回的是桶容量,不是当前剩余令牌数;真要暴露剩余数,得用r.ReserveN(c, 1)后查.OK和.Delay - Header 设置如
X-RateLimit-Remaining仅作参考,客户端不可信,别依赖它做逻辑分支
按 IP 或路径做多维度限流时必须隔离 Limiter 实例
共用同一个 *rate.Limiter 实例会导致所有请求互相干扰 —— 比如 A 用户被限,B 用户也跟着 429。必须为每个维度(如 c.ClientIP()、c.Request.URL.Path)维护独立实例。
推荐用 sync.Map 缓存,但要注意三点:
- key 类型统一用
string,避免指针或结构体做 key 导致无法命中 - 必须加过期清理机制,否则内存只增不减;可用
time.AfterFunc或后台 goroutine 扫描空闲超 5 分钟的 entry - 不要在
Allow()前对 map 加锁 ——rate.Limiter本身并发安全,锁只该用在 map 的读写上 - 如果维度组合复杂(如 “IP + 路径 + 用户 ID”),建议把拼接逻辑抽成函数,避免散落在多处出错
滑动窗口限流不能靠 time.Now() 算差值
单纯用 time.Now().UnixMilli() - lastTime 这类逻辑,在并发下会漏统计、窗口跳变、无法加权——本质是没分桶,也没对齐时间片。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
真正能落地的滑动窗口,得满足:固定分桶 + 原子计数 + 时间片对齐 + 加权求和。例如 1 秒窗口分 10 个 100ms 桶,每个桶用 *atomic.Int64,窗口起点必须用 time.Now().Truncate(shardDur) 对齐。
- 别用
sync.Map存桶 —— 它不支持原子增减,且 key 是时间戳,频繁创建/删除开销大 - 桶数组长度建议设为 10~100:太少(如 2)导致窗口抖动剧烈;太多(如 1000)增加锁竞争和内存占用
-
maxReq是理论上限,实际通过量可能略高 —— 因旧桶衰减、新桶未满,这是滑动窗口的正常行为,不是 bug - 整个
SlidingWindow结构体应初始化一次后注入中间件,不要在 handler 里每次 new
选令牌桶还是滑动窗口?看场景再决定
令牌桶适合大多数 API 限流:允许突发、配置简单、资源开销极小;滑动窗口适合强精度要求场景,比如支付回调、风控规则,但实现复杂、内存和 CPU 开销明显更高。
如果你的需求只是「单 IP 每秒最多 5 次」或「某接口全局限 100 QPS」,老老实实用 rate.Limiter 就够了。滑动窗口真正的价值在于「过去 60 秒内最多 100 次」这种动态窗口,而令牌桶的「每秒 100 次」是速率均值,两者语义不同。
最容易被忽略的一点:无论哪种算法,限流策略必须和业务 SLA 对齐。比如你承诺「99.9% 请求在 200ms 内返回」,那限流中间件本身的执行耗时就得压到 0.1ms 级别 —— 这时候 rate.Limiter 的无锁设计就比带 mutex 的手写桶更可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










