gin 中直接用 time.sleep 或全局计数器限流会因并发不安全导致 map 冲突、计数丢失和 goroutine 阻塞,正确做法是使用 golang.org/x/time/rate.limiter 实现线程安全的令牌桶限流,并按 ip 或路径等维度独立维护 limiter 实例,配合 sync.map 缓存与 ttl 清理。

为什么 Gin 里直接用 time.Sleep 或全局计数器做限流会出问题
因为并发不安全,且无法按 IP、路径等维度隔离统计。Gin 的 handler 是并发执行的,多个请求同时读写同一个 map 或变量,会导致 fatal error: concurrent map read and map write。更糟的是,没加锁时计数可能丢失,实际 QPS 远超预期。
- 别用未加锁的
sync.Map或普通map存计数 ——sync.Map不支持原子增减,得自己套sync.Mutex - 别在 handler 里直接调用
time.Sleep模拟限流 —— 阻塞协程,浪费 goroutine 资源 - 别把限流逻辑写死在路由里 —— 复用性差,不同接口要重复写
用 golang.org/x/time/rate 实现线程安全的内存限流中间件
rate.Limiter 是 Go 官方维护的令牌桶实现,轻量、无依赖、并发安全,适合单机场景。它不依赖外部存储,所有状态都在内存中,响应快,但重启后计数清零 —— 这对很多内部服务或开发环境是可接受的。
- 每个限流策略对应一个独立的
*rate.Limiter实例,避免多路复用冲突 - 推荐按
IP + path组合键生成 limiter(用sync.Map缓存),而不是全局一把锁 - 注意
limiter.Allow()返回false时应立即返回429 Too Many Requests,不要继续执行业务逻辑
// 示例:按 IP 限流,每秒最多 5 次
func RateLimitByIP() gin.HandlerFunc {
limiterMap := sync.Map{} // key: ip, value: *rate.Limiter
limit := rate.Limit(5)
burst := 5
return func(c *gin.Context) {
ip := c.ClientIP()
limiter, _ := limiterMap.LoadOrStore(ip, rate.NewLimiter(limit, burst))
if !limiter.(*rate.Limiter).Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
}
}
如何避免内存无限增长和冷启动抖动
sync.Map 不会自动清理过期 key,如果用户 IP 长期不访问,对应的 *rate.Limiter 会一直留在内存里。而新 IP 第一次访问时创建 limiter 也有微小延迟(虽然通常可忽略)。
- 给 limiter 加 TTL:用
time.AfterFunc定期清理空闲超过 5 分钟的 IP 条目(需额外维护访问时间戳) - 或改用固定大小的 LRU cache(如
github.com/hashicorp/golang-lru)替代sync.Map,限制最大 IP 数量 - 冷启动时可预热:在服务启动后 spawn goroutine,预先创建常用 IP 或默认 limiter
- 注意
rate.NewLimiter开销极小,但高频创建/销毁仍建议复用
Gin 中启用限流中间件的典型位置和顺序
限流必须放在认证、日志等中间件之前,否则未授权请求也能耗尽配额;也不能放在 panic recovery 之后,否则 panic 可能绕过限流检查。
- 正确顺序:
RateLimitByIP→AuthMiddleware→Logger→Recovery - 若需对某些路径放行(如健康检查),用
c.Request.URL.Path提前判断并c.Next()跳过 - 别在
router.Group().Use(...)里混用多个限流中间件 —— 它们会叠加生效,容易误触发 - 测试时用
ab -n 100 -c 20 http://localhost:8080/api验证是否真拦截,别只看代码逻辑
真正麻烦的不是写出来,而是键的设计粒度(IP?token?user_id?)和清理策略 —— 这俩没想清楚,跑两天内存就涨上去,然后才想起来查 pprof。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











