Go 推荐用 golang.org/x/time/rate.Limiter 实现令牌桶限流,因其轻量、线程安全、支持突发控制;需复用实例、合理设置 rate 和 burst(如 rate.Every(100ms), burst=20),并在路由前统一拦截,避免误用 Allow() 或 per-request 新建。

Go 用 golang.org/x/time/rate 做令牌桶限流最稳妥
标准库没限流,但官方扩展包 rate.Limiter 就是为这个场景设计的——轻量、线程安全、支持突发流量控制。别自己手写计数器或用第三方轮子,除非你明确要绕过它的精度/阻塞逻辑。
常见错误是把 rate.NewLimiter 当成“每秒放行 N 个请求”的开关,其实它默认允许突发(burst > 1),且 Allow() 不阻塞,Wait() 才会等令牌。用错就等于没限流。
-
rate.NewLimiter(rate.Every(100*time.Millisecond), 3)表示:平均 100ms 生成 1 个令牌,最多攒 3 个(burst=3) - 高并发下优先用
limiter.Wait(ctx),避免Allow()返回 true 后实际被后端压垮 - HTTP handler 中不要每个请求 new 一个
Limiter,必须复用实例,否则 burst 完全失效
HTTP middleware 里怎么嵌入限流逻辑
限流必须在路由分发前做,否则中间件顺序错乱会导致部分 handler 被跳过。别在 handler 内部调 limiter.Wait(),那是给单个业务逻辑用的,不是 API 网关级控制。
典型错误是把限流写在 http.HandleFunc 闭包里,导致每个路径都独立限流——而你真正想控的是总 QPS 或按 IP 分组。
立即学习“go语言免费学习笔记(深入)”;
- 用
http.Handler包装原始 mux,统一拦截:http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := limiter.Wait(r.Context()); err != nil { http.Error(w, "too many requests", http.StatusTooManyRequests); return } next.ServeHTTP(w, r) }) - 需要按 IP 限流?从
r.RemoteAddr提取客户端 IP(注意 X-Forwarded-For 可伪造,生产需结合真实 IP 提取逻辑) - 别在 middleware 里用
time.Sleep模拟等待——这会占 goroutine,limiter.Wait()是非阻塞调度,更省资源
rate.Limiter 的 burst 参数为什么不能设太小
burst 是令牌桶的“水池大小”,设成 1 就退化成严格匀速限流,但真实流量天然有毛刺。用户连续点两次刷新、前端重试、CDN 预热都会触发瞬时高峰,burst=1 会让合法请求直接 429。
另一个坑是误以为 burst 越大越宽松——其实它只影响“能容忍多大突发”,不影响长期速率。设 burst=100 但 rate=1qps,1 秒内还是最多处理 1 个请求,只是第 1 个请求能立刻拿到令牌,后面 99 个得排队等 99 秒。
- 合理 burst 值 ≈ 平均 RT × 期望并发数(例如平均响应 200ms,想撑住 5 并发,burst 至少设 5)
- rate 和 burst 要一起调:想控 10qps + 允许 20 并发突发 →
rate.NewLimiter(rate.Every(100*time.Millisecond), 20) - 测试时用
ab -n 100 -c 50 http://localhost:8080/api观察 429 比例,比看日志更直观
为什么不用 Redis 实现分布式限流
单机服务够用时,硬上 Redis 是给自己加延迟和故障面。rate.Limiter 是内存操作,每次判断几纳秒;Redis 至少一次网络 round-trip,P99 延迟可能翻倍,还引入连接池、超时、序列化等问题。
只有当你的 API 部署在多个 Go 进程或机器上,且必须全局统一配额时,才考虑 Redis。但这时要注意:Lua 脚本原子性 ≠ 无竞争,集群模式下 KEYS 分片可能让限流失效。
- 单机多 worker(如用
http.Server.Addr复用同一端口)→ 直接共享一个*rate.Limiter实例 - 多进程(systemd 多实例、K8s 多 Pod)→ 必须用外部存储,但优先选 Redis + Lua,别用 SETEX+INCR 组合(竞态漏洞已知)
- 如果选 Redis,用
redis-cell模块或CL.THROTTLE,比手写更可靠
复杂点在于 burst 的语义跨节点不一致——本地桶满不等于全局满,这点容易被忽略。


















