应直接用 golang.org/x/time/rate,它底层是线程安全、无锁、高精度的令牌桶实现,已通过大量生产验证;自行手写易出现并发漏判、时钟漂移误判及时间换算偏差超10倍等问题,且 time.Tick 模拟限流无法应对突发流量并存在 goroutine 泄漏风险。

限流中间件该用 golang.org/x/time/rate 还是自己手写令牌桶
直接用 golang.org/x/time/rate。它底层就是标准的令牌桶实现,线程安全、无锁、精度高,且已通过大量生产验证。自己手写容易在并发场景下漏判或误判(比如未用 atomic 操作计数器、忘记处理时钟漂移),还可能因时间单位换算错误导致实际限流值偏差 10 倍以上。
注意:不要用 time.Tick 配合全局计数器模拟限流——它无法应对突发流量,且 Tick 的 goroutine 泄漏风险真实存在。
如何让 rate.Limiter 在 HTTP 中间件里返回 JSON 错误而非 panic 或静默丢弃
rate.Limiter 本身不抛错,它只提供 Allow() 和 TryConsume() 这类布尔判断方法。错误响应必须由你主动构造并写入 ResponseWriter。
关键点在于:别用 Allow()(它不阻塞但也不告诉你剩余等待时间),改用 WaitN(ctx, n) 并捕获 context.DeadlineExceeded;或者更推荐用 ReserveN() + 显式检查 .OK(),再根据 .Delay() 决定是否拒绝请求。
- 如果
limiter.ReserveN(time.Now(), 1).OK() == false,说明当前被限流,立即写入http.StatusTooManyRequests - 务必设置
Retry-After响应头,值取int(reservation.Delay().Seconds()),前端可据此做退避重试 - 错误体建议用标准 JSON:
{"error": "rate limit exceeded", "retry_after": 1},别返回 HTML 或纯文本
不同路由路径怎么配不同限流策略(比如 /login 允许 100qps,/admin/* 只允许 5qps)
不能只在入口放一个全局 rate.Limiter。得按路径前缀或正则匹配动态选择限流器实例。
常见做法是预定义 map:map[string]*rate.Limiter,key 是路由模式(如 "login"、"admin"),value 是对应 QPS 的 limiter。中间件里用 strings.HasPrefix(r.URL.Path, "/admin/") 或 chi.RouteContext(r.Context()).RoutePattern()(如果你用 chi)提取路由标识,再查表获取 limiter。
注意:不要在每次请求里 new limiter——它内部有状态,必须复用。也不要为每个用户 IP 单独 new(除非真需要用户级限流),否则内存泄漏风险极高。
示例片段:
func rateLimitMiddleware(next http.Handler) http.Handler {
limiters := map[string]*rate.Limiter{
"login": rate.NewLimiter(100, 200), // 100qps,burst=200
"admin": rate.NewLimiter(5, 10),
"default": rate.NewLimiter(20, 40),
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := "default"
if strings.HasPrefix(r.URL.Path, "/login") {
key = "login"
} else if strings.HasPrefix(r.URL.Path, "/admin/") {
key = "admin"
}
limiter := limiters[key]
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(map[string]interface{}{"error": "rate limit exceeded"})
return
}
next.ServeHTTP(w, r)
})
}为什么加了限流中间件后压测显示 99% 请求延迟突增 200ms
大概率是你用了 WaitN(ctx, 1) 且没设超时。这个方法会阻塞直到拿到令牌,而默认令牌补充间隔是 1s / qps,比如 10qps 就是每 100ms 补一个,当 burst 耗尽后,第 11 个请求就得等满 100ms 才能进。
解决方案只有两个:
- 严格用
ReserveN()+OK()判断,绝不阻塞 - 如果真要支持“排队等待”,必须传带 timeout 的 context:
ctx, _ := context.WithTimeout(r.Context(), 50*time.Millisecond),然后limiter.WaitN(ctx, 1),超时就走拒绝逻辑
另外确认没在 limiter 初始化时把 burst 设成 1——这会让所有并发请求几乎必然排队,burst 至少设成 qps 的 2–3 倍才合理。


















