Gin 中直接使用 net/http 中间件会失效,因其执行机制不兼容——Gin 自维护 HandlerFunc 链,需用原生风格(接收 *gin.Context 并调用 c.Next()),限流须按 IP 分配 rate.Limiter 实例,分布式场景应通过 Redis+Lua 原子脚本实现。

为什么直接用 net/http 的中间件在 Gin 里会失效
因为 Gin 的中间件执行顺序和 net/http 原生中间件不兼容——Gin 自己维护了一套 HandlerFunc 链,直接把标准库中间件塞进去,c.Next() 不会被调用,计数逻辑就卡在半路。常见现象是:限流只生效一次,或者完全不触发。
- 必须用 Gin 原生风格写中间件:接收
*gin.Context,显式调用c.Next() - 不要试图包装
http.Handler,Gin 不识别它 - 限流状态(如计数器)不能存在 handler 闭包里,否则每次请求新建实例,计数清零
用 golang.org/x/time/rate 实现每秒 5 次的简单限流
rate.Limiter 是最轻量、无依赖的选择,适合单机场景。它基于令牌桶,精度高且内存开销小。但注意:它不是线程安全的全局实例,必须按 key(比如 IP)分开维护。
func RateLimit(limit rate.Limit, burst int) gin.HandlerFunc {
// 全局 map + sync.RWMutex 是常见错误写法 —— 并发写冲突风险高
// 正确做法:用 sync.Map 存储每个 IP 对应的 limiter
var limiters sync.Map
return func(c *gin.Context) {
ip := c.ClientIP()
limiter, _ := limiters.LoadOrStore(ip, rate.NewLimiter(limit, burst))
if !limiter.(*rate.Limiter).Allow() {
c.JSON(429, gin.H{"error": "too many requests"})
c.Abort()
return
}
c.Next()
}
}
-
rate.NewLimiter(5, 10)表示每秒最多 5 个令牌,初始桶容量为 10 - 不要用
time.Now()手动算窗口——Allow()内部已处理时间滑动 - 生产环境慎用
c.ClientIP(),需配合gin.ForwardedByClientIP = true和可信代理头
Redis + Lua 脚本做分布式限流时的三个关键点
单机限流扛不住集群流量,必须上 Redis。但直接用 INCR + EXPIRE 有竞态问题,必须用原子脚本。Gin 本身不绑定 Redis 客户端,选 github.com/go-redis/redis/v9 最稳妥。
- Lua 脚本必须返回整数:1 表示放行,0 表示拒绝(Gin 中据此判断是否
c.Abort()) - key 要带前缀和 TTL,例如
rate:ip:192.168.1.100:60s,避免不同策略 key 冲突 - 连接池 timeout 设置要小于限流窗口(比如窗口 60s,redis.DialTimeout 至少设为 5s),否则超时导致误拒
示例脚本逻辑(实际用 redis.Script 注册):
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if current == 1 then
redis.call("EXPIRE", key, window)
end
return current <= max and 1 or 0
为什么 gin-contrib/limiter 不推荐直接用于生产
这个库封装了内存和 Redis 两种后端,看起来省事,但默认内存模式用 map + sync.Mutex,没做 key 粒度锁,高并发下整个 map 被锁死;Redis 模式又硬编码了 EXPIRE 而非 SETEX,在 Redis Cluster 下可能路由失败。
- 它的
keyFunc返回空字符串时 panic,而很多用户没意识到需要显式处理未登录用户 - 错误码固定返回 429,无法自定义响应体(比如加
Retry-Afterheader) - 如果你已经用了它,至少重写
middleware.NewRateLimiter的 storage 参数,换成自己封装的sync.Map或redis.ClusterClient
真正稳定的方案,是自己封装一层薄胶水代码:用 rate.Limiter 处理单机,用 Lua+Redis 处理跨节点,边界清晰,出问题能快速定位到具体模块。

















