rate.limiter不是“每秒n次”的直觉用法,因为它基于令牌桶匀速填充+突发容量机制,如rate.newlimiter(rate.every(200ms), 3)表示每200ms补1令牌、桶最多存3个,控制最小间隔与突发上限,而非滑动窗口内总次数;滑动窗口需自行实现。

rate.Limiter 为什么不是“每秒 N 次”的直觉用法
直接写 rate.NewLimiter(rate.Every(200*time.Millisecond), 3) 并不等于“每秒最多 5 次”,而是“每 200ms 补 1 个令牌,桶最多存 3 个”。这意味着:第 1–3 个请求可以瞬间通过,第 4 个必须等满 200ms 才行——它控制的是**最小间隔 + 突发容量**,不是滑动时间窗口内的总次数。
常见误判场景:
- 用户连续刷新 3 次后卡住 200ms,以为限流失效或配置错了
- 压测时 QPS 看似达标,但实际是“3-0-3-0”脉冲式打满,下游仍可能被打垮
- 想表达“1 分钟内最多 100 次”,却用
rate.Every(600*time.Millisecond)+ cap=100,结果变成“每 600ms 最多放 1 个”,完全偏离语义
滑动窗口必须自己实现,uber-go/ratelimit 不是它
go.uber.org/ratelimit 是原子时钟版的单次间隔控制器,返回下一个允许调用的时间戳,不统计请求数、不维护时间窗口、不支持“N 秒内 M 次”这类语义。真要滑动窗口,得自己管时间切片和过期清理。
轻量可靠的做法(适用于单机、千级 QPS 场景):
- 用
[]time.Time存最近所有请求时间戳,配合sync.Mutex - 每次
Allow()前,先for循环从头部删掉time.Now().Add(-window)之前的戳 - 检查切片长度是否 limit,是则追加当前时间并返回 true
- 别用
map[int64]int按秒计数——固定窗口有临界突增,两秒交界处可能凑出 2×limit 流量
什么时候该选令牌桶,什么时候硬上滑动窗口
选 rate.Limiter 当你需要:
- 低延迟决策(核心路径无锁、纳秒级判断)
- 允许合理突发(比如登录接口允许连点 3 次再限速)
- 按 IP 或用户 ID 做细粒度限流(用
sync.Map[string]*rate.Limiter缓存实例即可)
选自研滑动窗口当你要:
- 精确回答“过去 60 秒内已处理多少请求”(比如运营后台实时看板)
- 防止短时尖峰(如 100ms 内 50 次刷屏请求),令牌桶对此无能为力
- 后续要对接动态阈值或规则引擎(滑动窗口天然是事实数据源)
注意:两者不是互斥关系。生产中常组合使用——滑动窗口做指标采集与告警,rate.Limiter 做快速放行决策。
漏桶在 Go HTTP 服务里基本不该用
漏桶强调恒定输出速率,HTTP 接口用户点击后没响应、没重试提示、只看到白屏或超时,会疯狂重试,反而加剧雪崩。它真正的落点是后台异步链路:
- 日志上报:用
chan *LogEntry做桶,worker 以固定 QPS 拉取发送 - DB 写入批处理:限制每秒最多 commit 几次,避免主库 wal 压力突增
- 邮件/短信网关调用:防止触发运营商频控封禁
如果非要用,别手写 time.Sleep 模拟漏水——高并发下 goroutine 泄漏风险极高;优先封装成 Submit(func()) bool 这类提交接口,内部用 time.Ticker 驱动消费。
真正难的不是选算法,而是明确你要控什么:是控平均速率?控瞬时峰值?控资源消耗节奏?还是控可观测性精度?把这个问题想清楚,剩下的只是代码实现细节。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











