golang.org/x/time/rate.limiter足以满足90%限流需求,官方维护、无锁并发安全、精度高;支持allow/reserven/waitn策略,可灵活组合ip或路径维度限流,需注意burst为桶容量而非剩余量,须用reserven获取实时状态。

直接用 golang.org/x/time/rate 就够了,别急着上第三方库——90% 的限流需求靠它就能干净解决,且无额外依赖、无锁、精度准。
为什么首选 golang.org/x/time/rate.Limiter
它不是玩具,是 Go 官方维护的生产级令牌桶实现。很多项目一上来就引入 gin-contrib/limiter 或 juju/ratelimit,但实际只是想限制「每秒最多 5 次请求」这种基础场景。自己封装中间件反而更可控:
-
rate.Limiter本身并发安全,不需要加锁,Allow()、ReserveN()、WaitN()可按需选策略(拒绝/排队/降级) - 第三方包常把 IP、路径、用户 ID 等逻辑硬编码进中间件,导致无法灵活组合——比如你只想对
/api/pay限流,但不关心来源 IP,这种需求官方Limiter配合路由分组就能解 -
rate.Every(time.Second / 10)表示「每 100ms 放一个令牌」,即 10 QPS;Burst()返回桶容量,不是剩余数——这点容易误用,真实剩余量得调ReserveN(time.Now(), 1)再查.OK()和.Delay()
全局限流:最简可用的 HandlerFunc 实现
适用于所有路由统一限速(如全站 API 最多 20 QPS),代码轻、无状态、开箱即用:
import (
"net/http"
"strconv"
"time"
"golang.org/x/time/rate"
"github.com/gin-gonic/gin"
)
<p>func RateLimitMiddleware(r <em>rate.Limiter) gin.HandlerFunc {
return func(c </em>gin.Context) {
if !r.Allow() {
c.Header("X-RateLimit-Remaining", "0")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
return
}
// 注意:这里用 r.Burst() 是示意,实际剩余数需 ReserveN 获取
c.Header("X-RateLimit-Remaining", strconv.FormatInt(int64(r.Burst()), 10))
c.Next()
}
}</p><p>// 使用:每秒最多 20 次,突发允许 40 次
r := rate.NewLimiter(rate.Every(time.Second/20), 40)
router.Use(RateLimitMiddleware(r))
</p>
关键点:
- 不要在
Allow()前加锁——rate.Limiter本身已并发安全 -
Burst()是桶容量,不是实时剩余;若需精确返回剩余数,必须用r.ReserveN(time.Now(), 1)判断并取.Tokens() - 这个写法不记录 IP 或路径,纯全局计数;如需区分维度,往下看
按 IP 或路径做细粒度限流:用 sync.Map 缓存 limiter 实例
每个客户端 IP 或每个路由路径都需要独立的 *rate.Limiter,否则会互相干扰。不能每次请求都 new 一个,得缓存 + 清理:
- 用
sync.Map存储key → *rate.Limiter映射(key 可以是c.ClientIP()或c.Request.URL.Path) -
rate.NewLimiter轻量,但 map 查找 + 初始化仍要控制频率;避免高频 key(如带 UUID 的路径)导致 map 膨胀 - 务必设置过期清理——例如用
time.AfterFunc或后台 goroutine 定期删掉空闲超 5 分钟的 limiter,否则内存无限增长 - 不要在获取 limiter 时加锁;
sync.Map.LoadOrStore是线程安全的
示例片段(仅核心逻辑):
var limiters sync.Map // key: string → *rate.Limiter
<p>func GetLimiter(key string, r rate.Limit, b int) <em>rate.Limiter {
if l, ok := limiters.Load(key); ok {
return l.(</em>rate.Limiter)
}
l := rate.NewLimiter(r, b)
limiters.Store(key, l)
// 后续应配定时清理逻辑
return l
}
</p>
别踩这些坑
几个高频出错点,直接影响限流是否生效:
-
rate.Every(time.Second / 5)≠ 每秒 5 次——它是「每 200ms 放一个令牌」,等价于 5 QPS;写成rate.Every(200 * time.Millisecond)更直观 - 用
Allow()时,它只判断「此刻能否取到令牌」,不阻塞也不排队;若想让请求等待(如秒杀排队),得改用WaitN(ctx, 1, timeout) - 没做 key 过期清理 → 内存泄漏;没做 key 归一化(如忽略 query 参数)→ 同一路径不同参数被当成多个 key,限流失效
- 在 Gin 中注册中间件顺序错误:限流中间件必须在
Recovery之后、业务 handler 之前,否则 panic 会绕过限流
复杂点在于「维度组合」——比如「同一 IP 对 /login 每分钟最多 10 次」,这时 key 得拼成 ip + "/login",且 burst 和 fill interval 要按时间窗口换算。这种场景下,手动管理比套用通用包更稳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











