结论是:别用time.ticker、别每次请求new rate.limiter、也别全局单例复用——这三类写法导致限流失效;因rate.limiter是有状态令牌桶,每次new即重置桶,burst归零、速率归零,无法跨请求累积令牌。

直接说结论:别用 time.Ticker,别在 handler 里 new rate.Limiter,也别全局只用一个 *rate.Limiter 实例——这三类写法在生产环境基本等于没限流。
为什么每次请求都 new 一个 rate.Limiter 就失效了
因为 rate.Limiter 是有状态的令牌生成器,不是开关。每次 new 都等于新建一个空桶,burst 归零、速率归零,所有请求都从“第一秒”开始算——它根本记不住上一个请求是谁、什么时候来的。
- 错误写法:
func(w http.ResponseWriter, r *http.Request) { limiter := rate.NewLimiter(10, 5); limiter.Wait(r.Context()) } - 正确做法:包级变量、结构体字段注入,或用
sync.Map按 key 缓存(如ip或user_id) -
rate.Limiter本身线程安全,但“复用”不等于“全局单例”;全站共用一个实例 = 所有用户共享配额,登录接口和支付接口被绑死在同一根绳上
Wait() 和 Allow() 在 HTTP 中间件里怎么选
绝大多数 Web 接口该用 Allow(),而不是 Wait(ctx)。前者是“查一下有没有令牌,没有就立刻拒”,后者是“排队等位”,会阻塞 goroutine —— 用户不该为限流逻辑多等 2 秒,网关超时后你还在等 token,协程就卡死了。
- 用
Allow():适合 API 入口,快速失败,返回429 Too Many Requests - 用
Wait(ctx):仅适合后台任务、非用户直连路径,且必须传入r.Context()(不能是context.Background()),否则超时控制完全失效 - 别写
if !limiter.Allow() { http.Error(...) }后再进业务逻辑——Allow()只判断瞬间状态,高并发下可能刚判完就被其他 goroutine 抢走 token;更稳的是WaitN(ctx, 1)或带重试的TryWait()
按 IP 或用户 ID 动态管理 *rate.Limiter 实例的坑
用 sync.Map[string]*rate.Limiter 是常见解法,但 key 泛滥是高频内存泄漏源——攻击者发 10 万个随机 X-User-ID,map 就膨胀 10 万条,没人清理。
- key 必须做白名单校验:比如限制长度 ≤32、只含字母数字,拒绝非法值
- 必须加 TTL 清理:启动后台 goroutine,每分钟扫描
sync.Map,对 5 分钟内未访问的 key 调用limiter.Reserve().OK()判断是否空闲,再决定是否Delete() - 注意反向代理场景:
r.RemoteAddr在 Nginx 后是上游地址,得解析X-Forwarded-For并做可信头校验,或 fallback 到fnv.Sum64String(ip)哈希匿名处理 - 别为每个请求路径参数(如
/user/123)都建一个 limiter;按前缀分组(/user/)更合理,避免 key 爆炸
burst 参数设多少才算合理
burst 不是缓冲区大小,而是“桶的最大容量”,它决定了你能容忍多大程度的突发流量。设太小(如 1)会让正常抖动也被拒;设太大(如 1000)等于限流形同虚设。
- 一般取 QPS 的 2–5 倍:QPS=10 → burst=20~50;QPS=100 → burst=200~500
- 支付类接口建议保守:QPS=5,burst=10;登录类可宽松些:QPS=50,burst=200
- 初始化时别用
rate.Every(1*time.Second)这种写法——它等价于rate.Limit(1),即每秒 1 次;要设 10 QPS,得写rate.Every(time.Second / 10)或直接rate.Limit(10)
真正难的从来不是调通 rate.Limiter,而是想清楚「谁该被一起限」——这个判断错了,后面所有配置都是徒劳。IP、用户 ID、API Key、路径前缀,选哪个做 key,取决于你的风控目标和信任边界,而不是技术能不能实现。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











