rate.Limiter必须复用,每请求新建实例会导致限流失效;全局限流用包级变量,按IP或用户ID限流用sync.Map缓存并定期清理;burst不宜为0,Wait()与Allow()语义不同,分布式场景需权衡是否引入复杂方案。

rate.Limiter 实例必须复用,不能每请求 new 一个
限流器失效的最常见原因,就是把 rate.NewLimiter 写在 handler 函数里。每次请求都新建实例,等于每个请求都有自己的“满桶”,完全不构成限制。
正确做法是:按限流粒度决定作用域——全局限流定义为包级变量;按 IP 或用户 ID 限流,则用 sync.Map 缓存,key 为清洗后的 IP(优先取 X-Forwarded-For 并校验可信跳数),value 是 *rate.Limiter。
- 别直接用
r.RemoteAddr当 key,代理环境下拿到的是内网地址 -
sync.Map不自动清理,长期运行需配合time.AfterFunc定期删除闲置 key(比如 1 小时未访问) - burst 值设为 0 会导致首次请求就被拒(初始桶空),通常设为 QPS 的 2–3 倍更稳妥
Wait() 和 Allow() 别混用,语义和行为完全不同
limiter.Wait(ctx) 是阻塞式等待令牌,适合要求强保序、不希望丢请求的场景(如支付回调);limiter.Allow() 是非阻塞试探,返回 false 就该立即返回 429,适合低延迟敏感接口(如搜索建议)。
-
Wait()会响应ctx.Done(),务必传带超时的 context(如context.WithTimeout(r.Context(), 500*time.Millisecond)),否则 goroutine 可能卡死 -
Allow()内部调用AllowN(now, 1),不阻塞也不重试,失败后不会预留或回滚状态 - 若需预占多个令牌(如按文件大小计费上传),用
limiter.ReserveN(time.Now(), n),失败记得调reserve.Cancel()
分布式部署下 rate.Limiter 失效,但别急着上 Redis
golang.org/x/time/rate 是纯内存实现,单机有效。3 个 Pod 部署时,用户发 30 QPS 就能绕过 10 QPS 限制——这不是 bug,是设计使然。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
是否需要分布式限流,得先看真实瓶颈:如果后端服务本身有集群限流能力(如 Nginx limit_req、API 网关层统一控制),Go 服务只需做单机兜底;如果必须由 Go 层强控,优先考虑轻量协调方案(如 etcd lease + 分布式计数器),而非直接套 Redis Lua 脚本。
- 自己实现滑动窗口+Redis 容易因网络抖动、pipeline 乱序、时钟不同步导致漏限或误限
- 多数业务场景下,“单机限流 + 后端服务自身限流”已足够,过早引入分布式复杂度得不偿失
- 若真要上,别复用同一套限流逻辑处理所有维度——IP 限流和用户 API Key 限流应走不同 key 前缀,避免互相干扰
HTTP 中间件里漏掉 X-RateLimit-* 响应头,等于没做完
只做放行/拦截不够,客户端需要知道还剩多少配额、何时重置,否则容易盲目重试触发雪崩。
- 用
limiter.Burst()和limiter.Limit()动态算剩余数,别硬编码 -
X-RateLimit-Reset应基于当前时间推算:time.Now().Add(time.Second / limiter.Limit()).Unix() - 别在
Allow()返回 false 后还执行后续逻辑——必须立刻http.Error(w, "...", http.StatusTooManyRequests) - 健康检查路径(如
/health)务必绕过限流,否则 k8s probe 失败会触发滚动重启
真正难的不是写对那几行 limiter.Wait(),而是想清楚:这个限流策略到底保护谁?是防爬虫、保后端、还是控成本?粒度选错了,再准的算法也白搭。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!










