rate.Limiter是Go限流最稳妥选择,因每次请求新建实例会导致每个请求拥有独立满桶,限流完全失效;正确做法是复用实例,按IP、用户ID或路径前缀等维度用sync.Map缓存,并优先使用Wait(ctx)配合超时控制。

rate.Limiter 是 Go 实现 API 限流最稳妥的选择,自己手写计数器或漏桶容易出竞态、时钟漂移、burst 失效等问题,直接用官方扩展包 golang.org/x/time/rate 即可。
为什么不能每次请求 new 一个 rate.Limiter
新建实例会让限流完全失效:每个 rate.Limiter 都从满桶开始(burst 初始值全可用),相当于给每个请求发“免检通行证”。它不是开关,而是有状态的令牌生成器,必须跨请求复用。
- 错误写法:
limiter := rate.NewLimiter(10, 5)放在http.HandlerFunc或 Gin 中间件闭包里 - 正确做法:定义为包级变量、注入到 handler 结构体,或用
sync.Map按 key 缓存(如 IP、用户 ID、路径前缀) -
rate.Limiter本身线程安全,但“复用” ≠ “全局单例”——全站共用一个实例 = 所有用户、所有接口共享同一配额,业务上通常不可接受
按什么维度分桶?key 怎么选才不踩坑
限流维度错了,算法再准也没用。核心是「谁该被一起限,谁就共享同一个 rate.Limiter 实例」。
- 按
r.RemoteAddr最简单,但 CDN 或 Nginx 反向代理后会变成同一个源 IP;生产环境应解析X-Forwarded-For并做可信校验,或配合X-Real-IP - 按用户标识(如
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()。
立即学习“go语言免费学习笔记(深入)”;
-
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(),减少唤醒开销
burst 参数设多少才算合理
burst 是令牌桶的“水池大小”,不是可容忍的并发数,而是应对真实流量毛刺的缓冲空间。
- 设成 1 就退化成严格匀速限流,用户连续刷新、前端重试、CDN 预热都会触发瞬时高峰,合法请求直接 429
- 设太大(如 1000)等于没限流;一般取 QPS 的 2–5 倍较稳妥(如限 10 QPS,
burst=30) - 初始化示例:
rate.NewLimiter(rate.Every(time.Second/10), 30)表示平均 100ms 生成 1 个令牌(即 10 QPS),最多攒 30 个 - 注意单位:
rate.Every(100 * time.Millisecond)等价于 10 QPS,别错当成毫秒级间隔直接填数字
实际部署时最容易被忽略的是:限流逻辑必须在路由分发前统一拦截,而不是写在某个 handler 内部——否则部分接口根本不受控;还有就是 burst 和 context 超时必须配合使用,否则 Wait() 可能永远等不到令牌。


















