time.Ticker不能直接当限流器用,因其无状态、仅提供平均时间间隔信号,无法控制突发流量、不支持阻塞等待或拒绝策略;真正限流需带状态的令牌桶,如rate.Limiter。
time.Ticker 不能直接当限流器用很多人一想到“每秒最多 N 次”,就立刻写 time.NewTicker(time.Second / time.Duration(N)),然后在 for range ticker.C 里处理请求。这看似合理,但实际会漏掉突发流量的控制:它只保证“平均间隔”,不防止单个时间窗口内瞬间涌入大量请求(比如前 10ms 就来了 N 个),也不支持阻塞等待或拒绝策略。
真正可控的限流必须带状态——能记录“还剩几个令牌”、“上次补充时间”、“是否允许通过”。time.Ticker 是无状态的定时信号源,不是限流器本身。
golang.org/x/time/rate 实现 token bucket 的标准姿势Go 官方扩展包 rate 提供了线程安全、低开销的令牌桶实现。核心是 rate.Limiter,它封装了桶容量、填充速率和当前令牌数。
rate.NewLimiter(rate.Every(100*time.Millisecond), 5) 表示“每 100ms 补 1 个令牌,桶最大容量 5”(即等效于 QPS=10,burst=5)limiter.Allow() 非阻塞判断:返回 true 表示立即放行,false 表示拒绝limiter.Wait(ctx) 阻塞等待,直到拿到令牌或 ctx 超时/取消package mainimport ( "context" "fmt" "time" "golang.org/x/time/rate" )
func main() { limiter := rate.NewLimiter(rate.Every(200time.Millisecond), 3) // 每 200ms 补 1 个,桶大小 3 for i := 0; i < 10; i++ { if !limiter.Allow() { fmt.Printf("request %d: rejected\n", i) continue } fmt.Printf("request %d: allowed\n", i) time.Sleep(50 time.Millisecond) // 模拟处理耗时 } }
rate.Limiter 的底层行为与常见陷阱rate.Limiter 默认采用“平滑填充”策略:每次调用 Allow 或 Wait 时,才按时间差计算应补充多少令牌(而不是后台定时 tick)。这意味着它非常轻量,但也会带来两个易忽略点:
rate.Every(d) 并非“每 d 时间固定补一个”,而是“补令牌的速率等效于每 d 时间补一个”,实际补充量是浮点精度计算的,所以严格来说是“平均速率”保障,不是硬实时周期rate.Limiter 实例——它要共享;也不要把它塞进 struct
里又忘记初始化(nil 的 *rate.Limiter 会 panic)rate 包自己实现 token bucket 的关键点极少数场景(如需记录详细拒绝日志、集成分布式存储、或强制要求“绝对整数令牌”)需要手写。此时必须守住三个底线:
sync.Mutex 或 atomic 保护令牌计数和上次更新时间,避免竞态time.Since(lastTime) 计算应补数量,并做截断(不能超过桶容量)绝大多数业务不需要自己写。官方 rate.Limiter 已足够健壮,且经过大量生产验证。自己实现容易在边界条件(如系统时间回拨、高并发争抢)上出错,得不偿失。