直接用 golang.org/x/time/rate,它基于经生产验证的无锁令牌桶,支持突发流量与平滑速率;自己实现易出并发错误,且性能与可靠性难保障。

限流该用 golang.org/x/time/rate 还是自己写令牌桶?
直接用 golang.org/x/time/rate,别自己实现令牌桶。它底层就是标准的令牌桶算法,经过大量生产验证,支持突发流量(burst)、平滑速率(rate.Limit),且无锁设计,性能足够应对万级 QPS。自己写容易在并发场景下出现计数偏差或 panic,比如漏掉 atomic 或误用 time.Ticker 导致 goroutine 泄漏。
关键参数注意:rate.NewLimiter 的第一个参数是每秒允许请求数(如 100),第二个是最大突发量(如 50)。突发量不是“缓冲区”,而是允许短时超发的令牌上限;设太小会拒绝合理抖动,设太大等于没限流。
-
limiter := rate.NewLimiter(100, 50)表示:长期均值 100 QPS,单次最多允许 50 个请求瞬时通过 - 若需按 IP 或用户 ID 区分限流,必须为每个 key 维护独立
*rate.Limiter实例,不能复用同一个 - 注意
limiter.Wait(ctx)会阻塞,而limiter.Allow()是非阻塞判断,微服务中推荐用后者 + 返回 429
HTTP 中间件里怎么嵌入限流逻辑?
在 Gin / Echo / net/http 的中间件中,提取请求标识(如 X-Real-IP、Authorization 头或路由参数),查对应限流器,再调用 Allow() 判断。不要把所有请求塞进一个全局限流器——那会把整个服务压成单点瓶颈。
常见错误:用 map[string]*rate.Limiter 但没加锁,导致并发写 panic;或用 sync.Map 却忘记定期清理过期 key,内存持续增长。
立即学习“go语言免费学习笔记(深入)”;
func RateLimitMiddleware(limiters sync.Map) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
v, _ := limiters.Load(ip)
limiter, ok := v.(*rate.Limiter)
if !ok {
limiter = rate.NewLimiter(10, 5)
limiters.Store(ip, limiter)
}
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, map[string]string{"error": "rate limited"})
return
}
c.Next()
}
}
如何避免限流器在高并发下成为性能瓶颈?
核心是控制限流器实例数量和生命周期。每请求都新建 *rate.Limiter 开销大;全量 IP 映射又可能爆炸式增长。折中方案是做 key 归约:比如只取 IP 前两段(192.168.*.*)、或对 user_id 取模分桶(userID % 100),再配固定大小的限流器池。
另一个坑是没设置清理机制。长时间运行后,sync.Map 里积压大量已下线客户端的限流器,GC 压力上升。建议搭配 time.AfterFunc 或后台 goroutine 定期扫描过期项(例如 30 分钟无访问则删除)。
- 不要用
time.Now().Unix()做 key,会导致每次请求都新建限流器 - 限流器本身不占多少内存,但每个实例含一个
time.Timer,大量实例会触发 timer heap 膨胀 - 若用 Redis 实现分布式限流(如
INCR + EXPIRE),注意网络延迟会让Allow()变慢,需设好超时和降级策略
为什么 rate.Limiter.Reserve() 很少用?
因为 Reserve() 返回的是 *rate.Reservation,需要手动调用 Delay() 或 OK(),逻辑绕且易出错。微服务接口通常要快速响应,要么放行要么 429,不需要预留后等待——那会拖慢整个链路。
典型误用:在中间件里调 res := limiter.Reserve() 后忘记检查 res.OK(),直接 c.Next(),结果本该拒绝的请求被放行了。
真正需要 Reserve() 的场景极少,比如后台任务调度要精确控制执行时间偏移,或者长连接中预占带宽。HTTP 接口限流,请坚持用 Allow() 或 Wait()。
限流不是越严越好,关键是识别真实攻击流量和业务抖动。上线前务必用 wrk 或 hey 模拟不同 burst 模式,观察 429 比例和 P99 延迟变化。很多问题出在 burst 设得太死,而不是算法本身。










