最轻量可控的限流方案是直接使用 time/rate.limiter,为每个 ip 绑定独立实例并用 sync.map 管理;需正确解析客户端 ip、避免重复创建、返回 429 状态码及标准限流响应头。

直接用 time/rate 是最轻量、最可控的选择,别一上来就堆 gin-contrib/limiter 或 Redis 方案——除非你真需要跨进程共享状态或分钟级精度统计。
用 time/rate.Limiter 做 per-IP 请求频次控制
这是 Gin 场景下最常用也最稳妥的限流方式,适合保护登录、注册、短信发送等敏感接口。核心是为每个 IP 绑定一个独立的 rate.Limiter 实例,靠 sync.Map 管理生命周期。
-
rate.NewLimiter(rate.Limit(10), 20)表示「每秒最多 10 次请求,桶容量 20」,允许短时突发(比如瞬间 20 次),但长期不会超速 - key 必须稳定:用
c.ClientIP()即可,但注意 Nginx 转发时要配X-Real-IP并在 Gin 中启用gin.SetMode(gin.ReleaseMode)+r.ForwardedByClientIP = true - 别在中间件里每次调
rate.NewLimiter—— 创建开销大且会覆盖旧实例;用sync.Map.LoadOrStore缓存已初始化的 limiter - 拒绝时必须返回
http.StatusTooManyRequests(429),并加响应头:X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset,否则前端没法做友好提示
自己写计数器中间件:按分钟窗口 + sync.Map
当你要的是「每分钟最多 N 次」这种整点对齐的统计(比如防暴力扫端口),time/rate 的秒级精度反而麻烦,手写更直观。
- key 设计用
ip + ":" + strconv.FormatInt(time.Now().Unix()/60, 10),天然按分钟切片,不用清理过期项 - value 存
struct{ count int; last time.Time },避免用map[string]int导致并发写 panic - 别用
time.Now().Format("2006-01-02 15:04")拼 key —— 格式化耗 CPU,且Unix()/60更快更稳 - 响应头里的
X-RateLimit-Reset应设为下一分钟开始的时间戳((time.Now().Unix()/60 + 1) * 60),不是当前时间加 60 秒
Redis + 计数器:跨实例共享限流状态
单机部署时没必要上 Redis;只有当你跑多副本、且必须保证「同一个 IP 在所有实例上总共不能超限」时才值得引入。
- key 用
"rate:ip:" + ip,EXPIRE设为窗口时长(如 60 秒),靠 Redis 自动过期,省去手动清理逻辑 - 别用
INCR+EXISTS两步操作 —— 有竞态;改用 Lua 脚本原子执行「加 1 并检查是否超限」 - 如果用
gin-vue-admin的limit_ip.go,注意它默认没做 IP 解析透传,Nginx 后需手动 setX-Real-IP到 context - Redis RTT 会增加平均延迟 1–3ms,高频接口慎用;压测时观察
redis-cli --latency是否稳定
别把「限速」和「限流」混在一起用
有人在 Gin 中间件里拿 rate.Limiter 控制下载带宽(bytes/sec),这注定失败——time/rate 只管请求数,不管 body 大小。
- 接口限流(requests/sec):保护后端逻辑,用
rate.Limiter或计数器即可 - 接口限速(bytes/sec):属于传输层控制,得靠反向代理(Nginx 的
limit_rate)或自定义 responseWriter 包装 body 流,跟 Gin 中间件不在一个层面 - 如果你真需要按流量限速,先确认是不是该由网关或 CDN 层处理,而不是在应用层硬扛
真正难的不是选算法,而是 IP 怎么取才准确、窗口怎么对齐才不被绕过、以及 429 响应头要不要暴露剩余次数——这些细节不调通,限流就只是个心理安慰。











