golang.org/x/time/rate 是 go 限流生产事实标准,uber-go/ratelimit 适用于严苛节奏后台任务;rate.limiter 的 allow() 仅瞬时判断不阻塞,wait() 配合 context 实现平滑控流;burst 至少为 1,分片限流需用 sync.map 缓存 per-key 实例。

Go 语言里做限流,golang.org/x/time/rate 是首选,不是“可以试试”,而是生产环境事实标准;uber-go/ratelimit 适合对节奏要求严苛的后台任务,但不适合 API 层快速失败场景。
rate.Limiter 的 Allow() 和 Wait() 别混用
Allow() 纯判断,不阻塞、不等待、不预留——它只看「此刻桶里有没有令牌」。返回 false 时你得自己处理拒绝逻辑,且后续请求不会因此“排队”。适合采样、日志降频这类容忍丢弃的场景。
Wait() 才是 API 限流的主力:它会阻塞,直到拿到令牌或上下文超时。配合 context.WithTimeout(r.Context(), 500*time.Millisecond),就能实现「等不及就拒」,避免请求长时间卡住。
- 别在中间件里写
if !limiter.Allow() { http.Error(...) }就完事——这等于放任突发流量打穿 burst 容量后直接 429,没削峰能力 - 真正要平滑控流,优先用
limiter.Wait(ctx),错误分支统一返回http.StatusTooManyRequests -
Reserve()+Wait()组合可用于需要预判执行时间的调度场景,比如定时任务排队
burst 参数设为 0 是个隐形炸弹
rate.NewLimiter(10, 0) 看似想限制严格到“零容忍突发”,实际效果是所有请求立刻被拒——因为桶初始容量为 0,又没令牌可生成(第一枚令牌要等 100ms 后才来),Allow() 永远返回 false,Wait() 则无限期阻塞(除非 context 超时)。
- burst 至少设为 1,否则限流器失去实用性
- burst 不是“越小越好”:它本质是缓冲区,值太小会导致正常波动也被拦截;值太大则削弱限流意义
- 典型配置如
rate.NewLimiter(rate.Every(time.Second/10), 5)表示「每 100ms 补 1 个令牌,最多攒 5 个」,能扛住短时 5 倍峰值
按用户 ID 或 IP 分片限流时别乱 new Limiter
全局单例 Limiter 只适用于全局限流(比如整个服务每秒最多 1000 请求)。一旦要做细粒度控制(如每个用户每秒最多 5 请求),必须按 key 分片,但不能为每个请求都 new 一个新实例——内存和 GC 压力会飙升。
- 用
sync.Map缓存 per-key 的*rate.Limiter,key 过期需主动清理(比如用带 TTL 的 LRU cache) - 注意
rate.Limiter本身线程安全,无需额外加锁 - 若 key 量极大(如百万级用户),考虑用布隆过滤器预筛 + 分片计数器兜底,避免 map 膨胀
uber-go/ratelimit 的 Take() 会强制引入延迟
ratelimit.New(10) 表示严格每 100ms 放行 1 个请求,无论当前系统负载多低。调用 Take() 会阻塞到下一个精确时间点,哪怕你只是第 1 个请求,也得等到 100ms 后才执行。
- 这种“硬节奏”适合调用支付网关、发短信等对外强依赖接口,防止被下游限流反制
- 绝不适合 HTTP API:用户感知到的是固定 100ms 延迟,而不是“快进快出 + 少量排队”
- 它不支持
burst,也不响应context取消——Take()一旦开始,就必须等到那个时间点
真正难的不是选哪个库,而是想清楚你要控的是什么:是防雪崩的弹性缓冲,还是保下游的刚性节拍。参数调错、方法用反、分片失控——这些坑都在业务压测时才暴露,但修复成本远高于设计时多花十分钟理清语义。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











