绝大多数场景下直接用golang.org/x/time/rate.Limiter即可——轻量、无依赖、线程安全、基于令牌桶;仅当需分布式限流或Prometheus指标时才引入第三方库。

限流该用 golang.org/x/time/rate 还是第三方库?
绝大多数场景下,直接用标准扩展包 rate.Limiter 就够了——它轻量、无依赖、线程安全,且底层基于令牌桶算法,能平滑应对突发流量。除非你明确需要分布式限流(比如多实例共享配额)、或需与 Prometheus 对接指标埋点,否则别急着引入 uber-go/ratelimit 或 go-redistore 这类库。
常见错误是把 rate.NewLimiter 实例在每次 HTTP 请求中新建,这会导致限流完全失效。正确做法是全局复用一个实例:
var apiLimiter = rate.NewLimiter(rate.Every(1*time.Second), 10) // 每秒最多10次
注意两个参数:rate.Every(1*time.Second) 控制填充速率(即“桶”多久加一个令牌),10 是初始容量(桶大小)。两者共同决定长期平均速率和短时突发能力。
HTTP 中间件里怎么安全调用 Allow() 或 Wait()?
别在中间件里直接写 if !limiter.Allow() { http.Error(...) }——这会漏掉并发竞争下的超限请求。更稳妥的是用 limiter.Wait(ctx) 配合上下文超时:
立即学习“go语言免费学习笔记(深入)”;
-
Wait()会阻塞直到拿到令牌或上下文取消,适合对延迟不敏感的接口 -
Reserve()+Delay()可预判等待时间,适合做排队提示 - 务必传入带超时的
context.Context,避免 goroutine 泄漏
示例片段:
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := apiLimiter.Wait(ctx); err != nil {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
按用户 ID 或 IP 做差异化限流,怎么管理多个 rate.Limiter 实例?
不能为每个用户都 new 一个 rate.Limiter——内存和 GC 压力会随用户数线性增长。推荐用带驱逐策略的 map + sync.Map:
- 键用
userID或ip.String(),值存*rate.Limiter - 用
sync.Map替代普通 map,避免读写锁争用 - 加个简单 LRU 逻辑:当 map size 超过阈值(如 10000),随机清理部分旧条目
注意:这种单机限流无法跨 Pod 生效。若服务部署在 Kubernetes 上且需全局一致限流,必须引入 Redis + Lua 脚本(例如用 redis-cell 模块),此时 rate.Limiter 就只适合做二级本地缓存。
测试限流逻辑时为什么总测不准?
核心原因是测试时没控制好时间精度和并发节奏。Go 的 time.Now() 在短间隔下受系统时钟粒度影响大;而快速起一堆 goroutine 发请求,又容易触发调度抖动。
实操建议:
- 单元测试里用
rate.NewLimiter(rate.Limit(10), 10)配合time.Sleep()精确控速,别依赖真实秒级周期 - 压测用
ab或hey时,加-z 30s参数跑足够长时间,避开冷启动抖动 - 检查日志里是否混用了
AllowN(time.Now(), n)和Wait()——它们内部时间基准不一致,混用会导致误判
真正难的不是实现限流,而是确认「这个 10 QPS 是指每秒处理完成数,还是每秒接收请求数」——前者看后端耗时,后者看网关入口。这两个数字在高延迟服务里可能差出一倍。










