Go限流首选golang.org/x/time/rate包,基于令牌桶模型,线程安全且轻量;需复用rate.Limiter实例,配合context.WithTimeout避免goroutine泄漏,分布式场景须用Redis等外部存储实现跨节点限流。

Go 限流用 golang.org/x/time/rate 最直接
标准库没有限流原语,但官方维护的 rate 包足够轻量、线程安全,且支持每秒/每毫秒粒度的请求配额控制。它基于令牌桶(token bucket)模型,比漏桶更适应突发流量。
关键类型是 rate.Limiter,构造时传入 rate.Limit(如 rate.Every(100 * time.Millisecond) 或 rate.Limit(10) 表示每秒 10 次)和桶容量(burst):
limiter := rate.NewLimiter(rate.Every(200*time.Millisecond), 3)
注意:burst 不是“最大并发数”,而是允许突发的请求数上限;超过后 Wait 会阻塞,Allow 直接返回 false。
-
limiter.Wait(ctx):阻塞直到获得许可,适合后台任务或可接受延迟的 API -
limiter.Allow():非阻塞判断,适合需要快速失败(如返回 429)的 Web 路由 - 不要在每次请求都新建
Limiter实例——它本身是并发安全的,应复用
HTTP 中间件里嵌入限流逻辑要小心 context 传递
常见错误是把 limiter.Wait(r.Context()) 放在中间件里,但没处理超时或取消。一旦上游调用方断开连接(比如前端关掉页面),r.Context() 会 cancel,此时 Wait 立即返回 error,但你可能没检查它,导致 500 错误。
立即学习“go语言免费学习笔记(深入)”;
正确做法是显式处理 cancel 和 timeout:
func RateLimitMiddleware(limiter *rate.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 100*time.Millisecond)
defer cancel()
if err := limiter.Wait(ctx); err != nil {
c.Header("X-RateLimit-Remaining", "0")
c.AbortWithStatusJSON(http.StatusTooManyRequests, map[string]string{"error": "rate limited"})
return
}
c.Next()
}
}- 务必用
context.WithTimeout控制等待上限,避免 goroutine 泄漏 - 别依赖
c.Request.Context()原生值——它可能已 cancel,且无超时保障 - Web 框架如 Gin/Echo 的中间件生命周期短,
Limiter必须从外部注入,不能在中间件内 new
单机限流够用,但分布式场景必须换方案
rate.Limiter 是内存级的,只对当前进程有效。K8s 多副本、多机器部署时,每个实例各自计数,总流量轻松突破阈值。
真要跨节点限流,得靠外部存储 + 原子操作:
-
Redis + Lua:用INCR+EXPIRE组合实现滑动窗口,或直接用 Redis 6.2+ 的CL.THROTTLE -
etcd:适合低频、强一致性要求场景,但性能不如 Redis - 避免用数据库(如 PostgreSQL 的
SELECT FOR UPDATE)——锁开销大,易成瓶颈
如果只是按 IP 或用户 ID 限流,注意 key 设计别太宽泛(比如只用 /api/order 作为 key 就失去区分意义),典型格式:rate:ip:192.168.1.100 或 rate:user:uid_12345。
别忽略限流指标暴露和可观测性
上线后没人知道限流是否生效、谁被拦了、burst 是否设得太小。至少暴露三个基础指标:
-
http_rate_limit_allowed_total{route="/api/v1/users"}:成功放行请求数 -
http_rate_limit_denied_total{route="/api/v1/users", reason="burst_exceeded"}:拒绝原因分类 -
http_rate_limit_wait_seconds{route="/api/v1/users"}:平均等待耗时(仅 Wait 模式)
用 prometheus/client_golang 注册 Counter/Gauge 即可,但关键点在于:这些指标必须和限流逻辑耦合在同一代码路径里——比如在 Allow() 返回 false 后立刻 deniedCounter.Inc(),而不是靠日志异步采集,否则丢数。
另外,burst 值不是越大越好。设成 100 可能掩盖真实过载,反而让下游服务雪崩。建议从保守值(如 2~5)起步,结合监控曲线逐步调优。










