go标准库无严格令牌桶实现,rate.limiter是滑动窗口漏桶;其burst非容量而是最大延迟时间×rate;手写需用atomic.int64、单调时钟和按需填充。

Go 标准库没有严格意义上的令牌桶实现,golang.org/x/time/rate 是漏桶变体;真要按 RFC 2697 实现带容量上限、独立填充速率、可透支的令牌桶,得手写或引入第三方库。
rate.Limiter 不是令牌桶,是“滑动窗口式漏桶”
很多人误以为 rate.NewLimiter(10, 5) 创建的是“每秒 10 个令牌、桶容量 5”的令牌桶——其实它底层不维护令牌计数器,而是靠时间戳推算“最早能放行的时间”。它的行为等价于:允许最多 5 次瞬时突发,之后强制压到平均 10 QPS。这在多数 HTTP 限流场景够用,但若需动态调速、跨 goroutine 观察剩余令牌、或与外部系统同步水位(比如和 Redis 联动),就撑不住了。
- 调用
Allow()或ReserveN()时,它计算的是“从 now 往前推 burst 所需时间是否已过”,不是实时查桶里还有几个令牌 -
burst参数本质是“最大允许延迟时间 × rate”,不是物理桶容量 - 传入错误的时间值(如截断到秒的
time.Now().Unix())会导致窗口错位、放行过多
手写线程安全 TokenBucket 的三个关键点
自己实现一个符合经典定义的 TokenBucket,核心是解决原子更新、按需填充、非阻塞获取。别用锁包全局状态,也别用 float64 累加令牌——浮点误差几小时后就能偏差几十个。
-
tokens字段必须是int64,用atomic.LoadInt64和atomic.CompareAndSwapInt64做 CAS 更新 - 填充逻辑不能依赖
time.Ticker,而应记录上次填充时间,在Take()时按时间差补:当前令牌 = min(容量, 上次值 + rate × 时间差),单位统一用纳秒+int64 - 拒绝时钟回拨:用
runtime.nanotime()替代time.Now().UnixNano(),它是单调时钟,不受 NTP 校正影响
漏桶算法必须“按需补桶”,不是“定时补桶”
用 time.Ticker 每秒往 channel 塞一个 token 看似简单,但完全失效——客户端 10ms 内连发 5 个请求,Ticker 还没 tick 第二次,桶就空了,全放行。真实漏桶必须每次请求都重算可用 token 数。
- 结构体里存
lastRefill int64(纳秒时间戳)和tokens int64,每次Allow()先更新:delta := (now - lastRefill) * rate / 1e9,再取min(capacity, tokens + delta) - 更新
lastRefill必须和tokens一起原子更新,否则并发下会丢失中间状态;推荐用sync/atomic.Value包裹整个状态 struct - 如果要用
time.Time,必须做if now.After(last) { ... }判断,否则服务器时间被 NTP 回拨会导致 token 暴增
真正难的不是写对单机版,而是把桶状态同步到分布式环境——比如多个 API 网关实例共享同一个桶水位。这时候 atomic 就没用了,得靠 Redis + Lua 原子脚本,或基于 Raft 的协调服务。单机 TokenBucket 写得再准,跨实例时桶还是各自为政。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











