应使用limiter.waitn(r.context(), 1)并确保ctx来自http.request原生上下文,避免goroutine泄漏;限流须在路由匹配后、业务前执行,且rate.limiter需按路径/用户分实例复用,禁止单请求新建。

Go HTTP中间件里怎么嵌入限流逻辑才不拖垮goroutine
直接在 handler 里调 limiter.Wait() 而不带 context,是线上 goroutine 泄漏的头号原因。客户端断连、网关超时、前端取消请求——这些都会让 Wait() 无限等下去,直到超时或进程重启。
必须用 limiter.WaitN(r.Context(), 1),且确保 r.Context() 是从 http.Request 原生携带的(不是手动 context.WithTimeout 包一层就完事)。
- 别自己起 goroutine 做限流判断:无法绑定请求生命周期,
ctx.Done()通知不到 - 别在中间件里用
time.AfterFunc或select+time.After模拟等待:精度差、易漏 cancel - 如果下游依赖 DB 或 RPC,建议把限流放在最外层中间件,避免限流前就卡在慢查询里
rate.Limiter 实例该定义成全局变量还是按路径/用户分实例
全局单例 rate.Limiter 只适合全站统一限流(比如整个 API 每秒最多 1000 请求)。真实业务中,90% 的场景需要分级控制:登录接口宽松、支付接口严格、管理后台接口更低。
按路径前缀分组,推荐用 map[string]*rate.Limiter + sync.RWMutex;按用户 ID 分,必须提取 key(如 r.Header.Get("X-User-ID")),再用 sync.Map 缓存 map[string]*rate.Limiter,避免每次加锁查 map。
- burst 参数设太小(如 1)→ 突发流量全被拒;设太大(如 1000)→ 形同虚设;一般取 QPS 的 2–5 倍
- 初始化
rate.NewLimiter(rate.Limit(10), 30)表示「每秒 10 个令牌,桶容量 30」,不是「每分钟」 - 别在每个请求里
new一个 limiter:每个实例独立计数,限流失效
为什么用 Allow() 判断后再处理请求是错的
Allow() 是非阻塞的,它只看当前有没有令牌,但不保证你拿到后下一毫秒不会被别的 goroutine 抢走。高并发下极易出现「判断通过 → 拿不到令牌 → 业务逻辑已执行一半」的竞态。
正确姿势只有两个:WaitN(ctx, n)(阻塞等待,自动感知 cancel)或 TryWaitN(n)(立即返回 bool,适合做快速拒绝)。
-
Allow()适合做采样日志或监控打点,不适合做准入控制 - 用
WaitN时,务必传入r.Context(),不要用context.Background() - 若需支持配置热更新(比如运营半夜调低阈值),用
atomic.StorePointer替换 limiter 实例指针,而不是 reload server
令牌桶 vs 漏桶:Web API 场景下选哪个
Web API 天然需要应对突发流量(比如秒杀、活动开抢、爬虫扫首页),漏桶强制恒定输出速率,会把合法的短时高峰也削平,用户体验差。令牌桶才是 Web 服务限流的事实标准。
golang.org/x/time/rate 就是令牌桶实现,轻量、线程安全、无锁(基于 atomic),压测单实例扛 5k+ QPS 没压力。除非你在做流量整形(如 CDN 回源限速),否则别碰漏桶。
- 固定窗口算法有临界问题,滑动窗口要自己维护时间戳或依赖 Redis,复杂度高
- 令牌桶的 burst 容量决定了系统能承受多大瞬时冲击,这个值得结合 P99 响应时间和平均并发估算,不能拍脑袋
- 如果目标是保护下游数据库,光靠 HTTP 层限流不够,还得在 DAO 层加连接池控制和 query timeout
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











