golang.org/x/time/rate足够应对95%单机限流场景,它轻量、线程安全、经生产验证;应复用limiter实例,按ip或用户id用sync.map分片缓存,合理设置limit(长期速率)和burst(瞬时容量,建议设为limit的2倍),优先使用wait(ctx)并传入带超时的context,避免allow()误用和全局单桶共享。

golang.org/x/time/rate 就够了,别折腾自研或第三方库。它轻量、线程安全、经多年生产验证,95% 的单机限流场景直接用它封装一层就能上线。
为什么不用自己写或引入 ulule/limiter、Sentinel-golang?
自己实现容易踩并发竞争、时钟漂移、burst 边界误判等坑;ulule/limiter 底层还是包装 rate.Limiter,多一层抽象没带来实质收益;Sentinel-golang 适合已接入 Sentinel 生态且需动态规则热更新的场景,单机限流纯属杀鸡用牛刀。
rate.NewLimiter 的 limit 和 burst 到底怎么配?
常见误配是 rate.Every(time.Second) 配 b=0,结果 Allow() 永远返回 false——因为桶初始为空,又不补令牌,直接拒掉所有请求。
-
limit是长期平均速率,类型是rate.Limit(即float64),比如rate.Limit(10)表示每秒 10 个令牌 -
burst是桶容量,不是并发数,而是允许的瞬时突增上限;设为1会极度僵硬,设为limit * 2更贴近真实流量节奏 - 冷启动时桶满,所以
burst也决定了首次能扛多少突发;但别设太大,否则内存占用和 GC 压力会上升
如何避免所有用户共享同一个桶?
直接在中间件里 new 一个 rate.Limiter,所有请求都走同一个实例,等于没限流——这是最常犯的错。
- 必须按维度(如
c.ClientIP()、r.Header.Get("X-User-ID"))做 key 分片 - 用
sync.Map缓存key → *rate.Limiter,避免全局锁;别用map + mutex,高并发下 mutex 成瓶颈 - IPv6 地址含冒号,
c.ClientIP()直接拼接会出错,得先标准化(比如用net.ParseIP后转字符串) - key 过期要清理,否则内存泄漏;可用定时 goroutine 扫描
sync.Map,或改用带 TTL 的 LRU cache
HTTP 中间件里 Allow()、Reserve()、Wait() 怎么选?
它们消耗令牌的行为完全不同,混用会导致超时、饥饿或误判。
-
Allow():非阻塞,只返回bool;适合“快速失败”场景(如登录防爆破),但无法获取等待时间 -
Reserve():返回*rate.Reservation,可调res.OK()和res.Delay();适合需要返回Retry-After头的场景 -
Wait():阻塞直到拿到令牌或 context 被 cancel;绝不能裸用Wait(context.Background()),必须传带 timeout 的子 context,否则请求可能永久挂起 - 批量请求用
AllowN()或WaitN(),注意:一次取 5 个令牌 ≠ 允许 5 个并发,而是这 5 个请求必须一起放行或一起拒绝
真正难的不是写代码,是 key 的提取逻辑是否覆盖所有代理链(比如 X-Forwarded-For 是否可信)、burst 值在不同业务接口下是否合理、以及 sync.Map 里那些没人访问的限流器什么时候被清理——这些细节不处理,插件上线后就会悄悄失灵。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











