官方推荐使用 golang.org/x/time/rate 实现限流,因其基于纳秒级精度令牌桶、并发安全、支持突发流量与上下文取消;避免手写计数器、channel 或 ticker 等易出错方案。

用 golang.org/x/time/rate 是唯一推荐做法
别自己手写计数器、channel 补令牌或 ticker 轮询——这些实现看似简单,但高并发下容易漏掉原子性、时钟漂移、burst 突发支持、上下文取消等关键细节。官方扩展包 rate 就是为这事写的,底层就是精度纳秒级的令牌桶,且 *rate.Limiter 并发安全,单实例可复用。
常见错误现象:
– 用 map[string]int + time.Now().Second() 做固定窗口,结果 00:59.9s 和 01:00.1s 的请求被算作两个窗口,瞬间翻倍放行
– 自己用 chan struct{} 实现,没处理好 select 默认分支与满桶丢弃逻辑,导致限流失效或卡死
-
rate.NewLimiter(r, b)中r是每秒补充速率(float64),b是桶最大容量(int);r=10, b=1不是“每秒 10 次”,而是“桶最多存 1 个令牌,每秒补 10 个”——第一秒只放过 1 次,其余全拒 - 想支持突发流量,
b至少设成和r相当,比如rate.NewLimiter(10, 10) - 用
rate.Every(100 * time.Millisecond)替代手算10.0,更不易错
Wait 和 Allow 到底该选哪个?
HTTP 中间件里,Wait 是默认首选;Allow 只适合极低延迟要求、允许快速失败的场景(如内部健康检查探针)。
常见错误现象:
– 用 Allow() 后只写 if !limiter.Allow() { http.Error(...) },但没记录日志、没返回标准限流头(如 X-RateLimit-Remaining),运维排查无从下手
– Wait 不传带 deadline 的 context,客户端断连后协程还在等,连接池慢慢耗尽
-
Wait(ctx)会阻塞直到拿到令牌或上下文超时/取消,天然匹配 HTTP handler 的串行模型 - 务必用
ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)包一层再传入,超时后直接返回http.StatusTooManyRequests -
Allow()是非阻塞的,只看当前有没有令牌,不排队也不等;它返回bool,没法透出剩余令牌数或等待时间
ReserveN 是唯一能扣多个令牌的接口
上传大文件、批量导出、GraphQL 单请求多操作等场景,一次要消耗多个配额,Allow 和 Wait 都只能扣 1 个,必须用 ReserveN。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
常见错误现象:
– 调了 res := limiter.ReserveN(now, 5) 后只判 res.OK(),却忘了调 res.Fulfill() 或 res.CancelAt(now),令牌被预占却不扣除,桶迅速变空
– 在循环里反复传不同时间戳给 ReserveN,比如每次用 time.Now(),导致同一请求内多次调用时间跳跃,限流行为飘忽
-
n必须 ≤burst容量,否则ReserveN直接返回不可用的*rate.Reservation -
n不能为 0 或负数,会 panic - 先
if !res.OK() { ... },再根据res.Delay()决定是否 sleep 或重试,最后必须显式res.Fulfill()(成功执行)或res.CancelAt(now)(放弃执行) - 时间戳建议缓存一次:在 handler 开头取
now := time.Now(),后续所有ReserveN/AllowN都复用它
per-user、per-ip 限流不能共用一个 *rate.Limiter
*rate.Limiter 没有上下文隔离能力,把它当全局单例用在多租户场景,等于所有用户共享同一桶,毫无意义。
常见错误现象:
– 用同一个 limiter 对所有 IP 做限流,结果一个恶意 IP 打爆桶,其他正常用户全被拦
– 用 sync.Map 缓存但没控制 key 生命周期,内存无限增长
- 按需创建:HTTP 中间件里解析
r.RemoteAddr或userID,查sync.Map获取对应*rate.Limiter,不存在则rate.NewLimiter(5, 5)新建并存入 - 加 TTL 清理:定期扫描
sync.Map,删除超过 10 分钟未访问的桶(可用time.Now()记录最后访问时间) - 路径级限流记得用
r.URL.Path匹配,别用r.RequestURI(含 query,易被绕过)
最常被忽略的点不是怎么写,而是时间戳一致性、Fulfill/ CancelAt 的配对、以及 burst 容量和实际业务突发能力的匹配——这三处一错,限流就从保护变成误伤。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










