应优先选用 Go 标准库的 rate.Limiter:轻量、线程安全、支持令牌桶,适用于大多数 API 防刷场景;避免过早引入第三方库,注意 burst 设置、全局复用实例、合理使用 WaitN、路径白名单及边界验证。

限流器选哪个:rate.Limiter 还是第三方库?
Go 标准库 golang.org/x/time/rate 提供的 rate.Limiter 足够应对大多数 API 防刷场景,不建议一上来就引入 gobreaker 或 uber-go/ratelimit。它的核心优势是轻量、无依赖、线程安全,且支持令牌桶(token bucket)语义——这正是防刷最需要的“突发流量容忍 + 平均速率控制”组合。
常见错误是直接用 time.Sleep 模拟限流,或自己用 sync.Mutex + 计数器实现,结果要么阻塞协程、要么并发不安全、要么无法处理突发请求。用 rate.NewLimiter 才能真正兼顾吞吐与防护。
-
rate.NewLimiter(10, 20)表示:每秒最多放行 10 个请求,但允许最多积压 20 个令牌(即短时突发可接受 20 次请求) - 初始化时不要把
burst设为 1 —— 这会把限流变成严格匀速,真实用户点击、重试、前端预加载都会被误杀 - 避免在 handler 内反复调用
limiter := rate.NewLimiter(...),每次 new 都是新桶,起不到限流作用;应全局复用一个实例
怎么嵌入 HTTP handler:别只看 AllowN,要懂 WaitN 的阻塞语义
很多教程只教用 limiter.AllowN 返回 bool 判断,但这会导致“请求立刻失败”,体验差且无法平滑削峰。更合理的方式是用 limiter.WaitN 主动阻塞超限请求,让它们排队等令牌,既保服务稳定,又不丢用户请求。
注意:WaitN 会阻塞当前 goroutine,但 Go HTTP server 默认为每个请求启一个 goroutine,所以不会影响其他请求 —— 这正是它的设计前提。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 对非关键接口(如搜索、列表页),用
err := limiter.WaitN(ctx, 1),配合ctx.WithTimeout控制最大等待时间(比如 500ms),超时则返回HTTP 429 - 千万别用
limiter.WaitN(context.Background(), 1)—— 缺失上下文取消机制,客户端断连后 goroutine 可能永久挂起 - 如果想区分用户级限流(如按 IP 或 token),不要把
rate.Limiter实例全局共享,而要用 map 或 sync.Map 缓存每个 key 对应的 limiter,但需注意 map 并发写 panic 和内存泄漏风险
如何避免漏掉关键路径:中间件里没覆盖 OPTIONS / healthz 就等于白加
限流中间件如果只 wrap 主业务 handler,会漏掉两类高频请求:CORS 预检的 OPTIONS 请求,以及探针类的 /healthz、/readyz。前者可能被恶意大量触发导致桶耗尽,后者若被限流会导致 k8s 认为服务不可用。
正确做法是在中间件开头做路径白名单判断,而不是在末尾才放行。
- 直接跳过
req.Method == "OPTIONS"或strings.HasPrefix(req.URL.Path, "/healthz")的请求,不走限流逻辑 - 不要用正则匹配路径做白名单 —— 每次请求都编译 regex 开销大,且易出错;用前缀判断或精确字符串比较更稳
- 如果用了 Gin/Echo 等框架,注意其
Use中间件顺序:限流中间件必须在日志、recover 之前注册,否则 panic 后限流状态已乱
上线前必须验证的三个边界点:burst、wait timeout、高并发下桶状态
本地跑通 curl 试几次不代表生产可用。真实压力下,rate.Limiter 的内部计数器和时间戳精度会影响行为,尤其当系统时间被 NTP 调整或容器内时钟漂移时。
最容易被忽略的是:burst 值设得过大(比如 1000),会让限流形同虚设;wait timeout 设得太长(比如 5s),会让客户端超时堆积,反而加剧雪崩。
- 用
ab -n 1000 -c 100 http://localhost:8080/api测试,观察实际 QPS 是否稳定在设定值附近,而非持续爬升 - 检查
limiter.Limit()和limiter.Burst()的运行时值,确认没被意外修改(比如配置热更新时未加锁) - 在 handler 中打印
limiter.ReserveN(time.Now(), 1).Delay()的返回值,看排队延迟是否随负载线性增长 —— 如果延迟突变或归零,说明桶状态异常

















