rate.Limiter是Go官方基于令牌桶算法的线程安全限流器,需复用实例而非每请求新建;应按用户/IP分桶避免锁竞争,HTTP中间件中须在路由匹配后、业务前调用Wait()并传入request context。

用 golang.org/x/time/rate 实现令牌桶限流
Go 官方扩展库 rate.Limiter 是最常用、最轻量的限流方案,底层基于令牌桶算法,线程安全,适合 HTTP 中间件场景。
常见错误是直接在 handler 里 new 一个 rate.Limiter,导致每个请求都用独立限流器,完全失效。正确做法是复用同一个实例:
- 按用户 ID 或 IP 做 key 分桶限流?需配合
sync.Map或第三方缓存(如ristretto),避免全局锁瓶颈 -
rate.Every(1 * time.Second)表示每秒放行 1 个令牌;rate.Limit(10)表示每秒最多 10 次——两者等价,但前者更直观 - 注意
WaitN(ctx, n)的n是请求数量(比如批量接口),不是并发数;单次请求固定传1
HTTP 中间件中嵌入限流逻辑
限流必须在路由匹配后、业务处理前触发,否则可能绕过或误杀健康请求。典型中间件写法如下:
func RateLimitMiddleware(limiter *rate.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
if err := limiter.Wait(c.Request.Context()); err != nil {
c.JSON(429, gin.H{"error": "too many requests"})
c.Abort()
return
}
c.Next()
}
}关键点:
立即学习“go语言免费学习笔记(深入)”;
- 用
Wait()而非Allow():前者会阻塞等待令牌(适合突发容忍),后者只查当前是否允许(适合硬拒绝) - 务必传入
c.Request.Context(),否则超时控制和 cancel 无法生效 - 如果用
gin,不要在c.Abort()后调用c.Next(),否则仍会执行后续 handler
高并发下 rate.Limiter 的性能与陷阱
rate.Limiter 单实例压测可达 10w+ QPS,但实际部署容易踩坑:
- 共享一个
rate.Limiter实例时,所有请求串行竞争同一把锁 —— 这是设计使然,不是 bug;若需更高吞吐,必须分桶(如按 user_id % 64) - 时间精度依赖系统时钟,容器环境(尤其是 CPU 资源受限的 Kubernetes Pod)可能出现时钟漂移,导致限流松动
- 不支持分布式限流:多实例部署时,各节点各自计数,总流量 = 单节点 × 实例数。跨节点需接入 Redis + Lua(如
redis-cell)或专用服务(如 Sentinel)
简单测试限流是否生效的技巧
别等上线后看监控,本地快速验证更可靠:
- 用
ab -n 20 -c 10 http://localhost:8080/api发起 20 次请求、并发 10,观察返回 429 的比例是否接近预期(如每秒限 5,则约 15 次被拒) - 在限流中间件里加日志:
log.Printf("limiter allow=%t, remaining=%.2f", limiter.Allow(), limiter.Limit()),注意Allow()不影响内部状态,仅做快照 - 临时把
rate.Every改成time.Millisecond * 100,让限流效果秒级可见,避免干等
限流本身不难,难的是边界清晰:单机还是分布式、突增容忍还是严格硬限、按请求维度还是资源维度(如 DB 连接数)。选错模型,再好的代码也救不回来。










