核心是用独立goroutine周期性向chan struct{}发送令牌,逻辑清晰、无锁、内存占用低;struct{}零开销且语义准确;需校验qps>0;len(tokench)不可用于判断可用令牌;burst典型值为qps×2。

用 time.Ticker + chan struct{} 启动补令牌 goroutine
核心是让一个独立 goroutine 周期性往 channel 里塞令牌,而不是在每次请求时计算时间差。这样逻辑清晰、无锁、内存占用低。
-
chan struct{}比chan int更轻量——它零内存开销,语义也更准确(只表“有令牌”,不关心数值) -
time.Second / time.Duration(qps)是补令牌间隔,但qps == 0会 panic,上线前必须校验 - 不要用
len(tokenCh)判断剩余令牌数:它只反映缓冲区当前长度,不等于可用令牌(可能有 goroutine 正卡在select的发送分支上) -
burst值太小会导致突发流量全被拒;太大则失去限流意义;典型取值为qps * 2
Allow() 函数里为什么不能直接 select { case
看似简洁,但这是阻塞式调用,一旦 channel 空,goroutine 就挂起——HTTP handler 里这么写,会导致连接堆积、RT 突增、goroutine 泄漏。
- 正确做法是加
default分支做非阻塞尝试:select { case - 如果业务允许等待,应由调用方控制重试或 sleep,而不是在限流器内部阻塞
- 尤其注意:HTTP handler 中若用
limiter.WaitN(ctx, 1)却没设context.WithTimeout,上游断连后 goroutine 仍等令牌,造成泄漏
闭包封装 vs 结构体封装:状态怎么存才安全
闭包能天然捕获 tokens 和 last 等变量,适合单例、无共享场景;但并发读写 tokens 必须加锁或用 atomic,否则竞态导致限流失效。
- 常见错误是把
tokens当参数传入再返回——Go 值传递,每次都是新副本,状态无法延续 - 用
sync.Mutex最简单,但高并发下atomic.CompareAndSwapInt64循环更新更高效 - 别在闭包里用
time.Sleep等令牌,这会让整个 goroutine 阻塞;Allow()应只做原子判断,快进快出 - 初始令牌建议设满桶(
capacity),否则首次请求可能因桶空被误拒
为什么 golang.org/x/time/rate 直接用在 HTTP 中间件容易翻车
它默认设计偏“服务端整形”,而 Web 服务更需要“客户端友好”的快速失败机制。
- 别在 handler 里调
limiter.WaitN(ctx, 1)—— 一卡就是几百毫秒,压测时 RT 直线上升 - 改用
limiter.AllowN(time.Now(), 1),配合 429 响应和X-RateLimit-*头,让客户端知道还能等多久、剩几个令牌 - 每个路径或用户要配独立
*rate.Limiter实例,不能共用全局一个;用sync.Map存,键名建议哈希化防爆炸 -
burst设太大(比如等于qps * 10)会让限流形同虚设;设太小(如qps)又扛不住正常毛刺
实际最难的不是写对一个桶,而是让成百上千个桶在高并发下不抢缓存行、不拖慢主流程、不漏掉任何一次拒绝,并且能让运维一眼看懂当前压在哪条路线上。这些细节藏在 atomic 循环的边界检查里,藏在 sync.Map 的惰性淘汰逻辑里,也藏在你返回的每一个 X-RateLimit-Reset 时间戳是否真正对应桶的重置时刻。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











