gin 默认不带限流因其定位为轻量路由框架,限流需自行实现或引入第三方方案;单机用golang.org/x/time/rate,分布式用redis+lua脚本保证原子性。

为什么 Gin 默认不带限流,而要用第三方库
Gin 本身是轻量 HTTP 路由框架,不内置限流逻辑——它连中间件都只提供注册机制,具体策略(如令牌桶、漏桶、滑动窗口)得自己实现或引入成熟方案。硬写一个 time.Sleep 或简单计数器,在并发场景下容易因竞态或时钟漂移失效,且无法跨进程共享状态。生产环境通常选 golang.org/x/time/rate(单机)或 redis + Lua(分布式),前者够用且无外部依赖。
用 rate.Limiter 实现每秒最多 10 次请求的中间件
核心是把 rate.NewLimiter 实例挂到 Gin 的 gin.Context 上,每次请求调用 Allow 或 Wait 判断是否放行。注意:不能每个请求都新建 Limiter,必须复用;也不能直接在 func(c *gin.Context) 里初始化,否则所有路由共用同一个限流器(除非你真想全局限流)。
常见错误现象:429 Too Many Requests 响应没返回、限流阈值始终不准、高并发下 panic(因 rate.Limiter 非 goroutine-safe 初始化)。
- 按路由路径单独配置限流器:用
sync.Map缓存不同c.FullPath()对应的*rate.Limiter - 使用
limiter.Wait(c.Request.Context())而非Allow(),避免因时钟误差导致瞬时超发 - 在
Wait返回 error 时显式写入c.AbortWithStatusJSON(429, gin.H{"error": "rate limited"}),否则 Gin 会继续执行后续 handler - 若需区分用户/IP,把 key 改成
c.ClientIP() + ":" + c.FullPath(),但注意 IPv6 地址长度和冒号冲突问题
func RateLimitMiddleware(rps int) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Limit(rps), rps)
return func(c *gin.Context) {
if err := limiter.Wait(c.Request.Context()); err != nil {
c.AbortWithStatusJSON(429, gin.H{"error": "rate limited"})
return
}
}
}
当需要分布式限流时,为什么 Redis + Lua 是更稳的选择
单机 rate.Limiter 在多实例部署下完全失效——每个进程维护自己的桶,总请求量轻松突破阈值。Redis 的原子性 + Lua 脚本能保证“读-判-写”一步完成,且支持精确时间窗口(如每分钟 60 次)。但要注意:redis.SetNX 类命令无法实现滑动窗口,必须用 INCR + EXPIRE 组合,或直接用 Redis 6.2+ 的 INCRBYEX。
性能影响明显:每次请求多一次 Redis RTT,若 Redis 延迟高或连接池不足,反而拖慢整体响应。建议设置超时(context.WithTimeout)并降级为本地限流(fallback)。
- 脚本中用
KEYS[1]表示限流 key(如"rl:ip:192.168.1.1:/api/v1/users"),ARGV[1]传窗口秒数,ARGV[2]传最大请求数 - 不要用
GETSET,它无法处理过期逻辑,INCR后立刻EXPIRE才能保证窗口重置 - 若用
github.com/go-redis/redis/v9,记得用script.Load().Run()预加载脚本提升性能
别忽略限流器的初始化时机和生命周期
最容易被忽略的是:限流器对象该在哪儿创建?在 main() 函数里 new 一个全局变量看似省事,但所有路由共享同一套速率,无法差异化控制;若在中间件闭包里 new,又可能因闭包捕获变量导致内存泄漏或误共享。
真实项目里,你应该按业务维度分组管理限流器——比如登录接口用 5rps,查询接口用 100rps,后台管理接口不限流。这时推荐用 map[string]*rate.Limiter + sync.RWMutex 动态注册,或者直接把限流配置写进 YAML,启动时解析生成对应实例。
另外,rate.Limiter 内部有定时唤醒 goroutine(用于桶内 token 自动补充),虽然开销极小,但如果创建几千个不同限流器,goroutine 数量会失控。此时要么合并相近策略,要么改用无状态的 Redis 方案。











