rate.Limiter是Go最稳妥的单机限流方案,每次请求new实例会导致限流失效,因每个新实例桶初始满载,等同免检;必须复用实例,按IP、用户ID或路径前缀等维度用sync.Map缓存,配合理想burst值(QPS的2–5倍)和Wait(ctx)正确调用。

rate.Limiter 是 Go 里最稳妥的单机限流选择,别自己手写计数器或用 time.Ticker 拼逻辑——前者容易竞态和时钟漂移,后者固定窗口漏判突发流量,上线即翻车。
为什么每次请求 new 一个 rate.Limiter 就失效
每个新实例都从满桶开始(burst 初始全可用),相当于给每个请求发“免检通行证”。它不是开关,而是有状态的令牌生成器,必须跨请求复用。
- 错误写法:
limiter := rate.NewLimiter(10, 5)放在 handler 或 Gin 中间件闭包里 - 正确做法:定义为包级变量、注入 handler 结构体,或用
sync.Map按 key 缓存(如 IP、X-User-ID、路径前缀) -
rate.Limiter本身线程安全,但“复用” ≠ “全局单例”——所有用户共用一个实例 = 全站共享配额,业务上通常不可接受
按什么维度分桶?key 怎么选才不踩坑
核心是「谁该被一起限,谁就共享同一个 rate.Limiter 实例」。选错 key,算法再准也没用。
- 按
r.RemoteAddr最简单,但 Nginx/CDN 后全是同一个地址;应优先解析X-Real-IP或可信的X-Forwarded-For首段 - 按用户标识(如
r.Header.Get("X-User-ID"))更精准,但需确保 header 不可伪造;匿名用户可 fallback 到 IP 哈希(fnv.Sum64String(ip)) - 按路径前缀(如
/api/pay、/api/login)适合分级管控:支付接口严控 5 QPS,登录接口宽松 50 QPS - key 泛滥是高频坑:攻击者构造随机
X-User-ID,sync.Map无限膨胀;务必加约束——只接受 32 字符内、仅含字母数字的 ID,并配后台 goroutine 定期清理 5 分钟未访问的条目
Wait() 和 Allow() 到底该用哪个
绝大多数 HTTP API 场景该用 limiter.Wait(ctx),而不是 limiter.Allow()。
-
Allow()只查“此刻有没有令牌”,返回 true 后其他 goroutine 可能瞬间抢走它,导致实际超限;适合非关键路径(如埋点上报)或前置快速拒绝(但得自己算Retry-After) -
Wait(ctx)是真正意义上的“排队等位”:阻塞直到拿到令牌,或上下文超时(如客户端设了 3s timeout);自动尊重ctx.Done(),不会卡死 goroutine - 务必传入
r.Context()(net/http)或c.Request.Context()(Gin),别用context.Background()——否则超时控制彻底失效,连接堆积,服务雪崩 - 高并发下建议用
WaitN(ctx, n)替代多次Wait(),减少唤醒开销(n是请求数量,比如批量接口)
burst 设为 0 会怎样?怎么设才合理
burst 是令牌桶的“初始容量”和“最大积压数”,不是并发数。设成 0 意味着桶里永远没有可用令牌,Wait() 和 Allow() 全部立刻返回 false 或阻塞超时。
立即学习“go语言免费学习笔记(深入)”;
- 生产环境常见误配:
rate.NewLimiter(10, 0),结果所有请求 429 -
burst至少设为 1,推荐值为 QPS 的 2–5 倍(如 10 QPS →burst=20) -
burst太小(如 1)会导致正常重试、客户端抖动也被拦截;太大(如 1000)等于弱化限流效果 - 它影响的是“短时突发容忍度”,不是长期速率——长期仍受
rate.Limit约束
rate.Limiter 天然不适用,必须换 Redis + Lua(如 redis-cell)或专用服务;但单机限流最容易出问题的地方,从来不是算法本身,而是 key 管理没做约束、burst 配成 0、context 传错,或者以为 Wait() 能自动中断——其实它只在 ctx Done 时才退出,上游没设 timeout 就等于没限。


















