直接用golang.org/x/time/rate.Limiter就够了,因其线程安全、经压测、无锁、纳秒级精度;需按IP或用户ID用sync.Map隔离实例,避免全局复用,并根据场景选用Allow()、Reserve()或Wait()方法。

单机场景下用 golang.org/x/time/rate 就够了,别一上来就上 Redis 或第三方库——多数内部接口、管理后台、低流量 API 完全不需要分布式限流。
为什么直接用 rate.Limiter 而不是自己写令牌桶
标准库的 rate.Limiter 已经是线程安全、经过压测的生产级实现,底层用原子操作+时间轮优化,比手写 sync.Mutex + time.Now() 更准、更轻、更少出错。自己实现容易漏掉“令牌累积精度丢失”“并发抢 token 导致超发”这类边界问题。
-
rate.NewLimiter(rate.Limit(10), 5)表示:长期速率 ≤10 QPS,瞬时最多允许 5 个请求一起进来(突发容量) - 它不依赖外部存储,无网络开销,冷启动零延迟
- 但注意:
rate.Limiter实例不能全局复用 —— 所有请求共用一个桶,等于没限流
按 IP 或用户 ID 隔离限流桶,必须用 sync.Map
限流失效最常见的原因是“所有请求共享同一个桶”。比如你写了 var limiter = rate.NewLimiter(...) 然后在中间件里直接调 limiter.Allow(),结果所有用户排队等同一把令牌。
- 正确做法:用
sync.Map缓存每个c.ClientIP()对应的*rate.Limiter - key 建议用
c.ClientIP()(IPv6 自动转为标准格式),避免用c.Request.Header.Get("X-Forwarded-For")—— 这个头可伪造,防刷意义不大 - 不用
map + mutex:高并发下锁争用严重,sync.Map在读多写少场景下性能高出数倍 - 不用自动过期清理:短连接场景下连接断开即自然淘汰;若需主动清理,加个定时 goroutine 扫描旧 key 即可,别用
time.AfterFunc绑定每个桶 —— 内存泄漏风险高
Allow()、Reserve()、Wait() 到底怎么选
这三个方法行为差异极大,选错会直接导致超时、误拒或服务卡死。
-
limiter.Allow():非阻塞,返回bool。适合登录、短信发送等“快速失败”接口,没令牌立刻返回429 -
limiter.Reserve():返回*rate.Reservation,可查r.OK()和r.Delay()。适合前端需要提示“请 x 秒后重试”的场景 -
limiter.Wait():阻塞等待令牌,**必须传带 timeout 的context.Context**。例如ctx, cancel := context.WithTimeout(c.Request.Context(), 100*time.Millisecond),否则恶意客户端可能让 goroutine 永久挂起 - 批量请求别用
AllowN(5)当“允许 5 并发”——它表示“这 5 个请求必须同时放行或同时拒绝”,和并发控制不是一回事
上线前必须检查的三个坑
限流中间件上线后突然不生效,90% 是栽在这三处。
- 没对
c.ClientIP()做标准化:Nginx 反向代理后,c.ClientIP()默认是127.0.0.1,得配engine.ForwardedByClientIP = true并设置engine.TrustedProxies - 限流放在了
router.Use()全局中间件里,但静态资源(/static/、/favicon.ico)也被限——加c.Request.URL.Path白名单过滤 - 测试时用
curl -H "X-Forwarded-For: 1.1.1.1"伪造 IP,但生产环境没开信任代理,导致所有请求都识别为同一个 IP
限流不是加个中间件就完事,关键在隔离粒度和上下文控制。桶本身很轻,难的是怎么让每个合法用户拿到自己的桶,又不让恶意请求钻空子。


















