最稳妥的选择是使用 golang.org/x/time/rate.limiter,而非手写 channel + ticker 或依赖 goroutine 数量“自然”限速;goroutine 仅为调度单位,不感知真实资源瓶颈,需外部协调机制实现可靠限速。

直接说结论:用 golang.org/x/time/rate.Limiter 是最稳妥的选择,不是自己手写 channel + ticker;goroutine 本身不提供速率控制能力,它只是执行单元,限速必须靠外部协调机制。
为什么不能靠 goroutine 数量“自然”限速
很多人误以为“起 10 个 goroutine 就等于并发 10”,但这是错的:goroutine 是调度单位,不是资源计量单位。它不感知 CPU、网络、数据库连接等真实瓶颈;runtime.NumGoroutine() 是瞬时快照,无法防止瞬间涌进 1000 个请求后全起 goroutine 导致文件描述符耗尽或 DB 连接池打满。
- 每个 goroutine 至少占 2KB 栈内存,上万并发时仅栈就吃掉 20MB+
- HTTP handler 里写
go handle(r)是典型反模式,没上下文取消、没超时、没排队缓冲 - goroutine 起得再快,也拦不住下游服务(比如 PostgreSQL)在 1 秒内被塞 5000 条
INSERT
rate.Limiter 怎么用才对:初始化和复用是关键
错误做法是每次请求都 rate.NewLimiter(10, 5) —— 每次新建实例,桶是空的、时间没推进,限速完全失效。
- 必须定义为包级变量或 struct 字段,全局复用:
var limiter = rate.NewLimiter(rate.Limit(10), 5) -
rate.Limit(10)表示长期平均 10 token/秒,5是桶容量(最大突发请求数) - Burst 至少 ≥ Limit,否则新速率永远达不到;比如要支持 10 QPS 突发,Burst 设成 20 更合理
- 别手算
10.0,用rate.Every(time.Second / 10)更直观、不易出错
并发数限制 vs QPS 限制:填 0 还是填正数
rate.Limiter 既能控 QPS,也能控瞬时并发数,区别全在第一个参数:
- 控 QPS(如 API 每秒最多 10 次):
rate.NewLimiter(rate.Every(100*time.Millisecond), 10) - 控并发数(如最多同时发 5 个 HTTP 请求):
rate.NewLimiter(0, 5)—— 填0表示令牌不自动补充,桶容量就是硬上限 - 调
limiter.Wait(ctx)才真正阻塞排队;只调Allow()或ReserveN()不等待,等于没限 - 务必传带超时的
ctx,例如context.WithTimeout(r.Context(), 100*time.Millisecond),避免 goroutine 卡死
上传/下载这类字节流限速,别用 rate.Limiter
rate.Limiter 的单位是“次数”,不是“字节”。拿它去限上传速度,会把 1MB 当作 1048576 次请求处理,逻辑直接崩。
- 字节级流控该用
github.com/juju/ratelimit:ratelimit.NewBucketWithRate(1*1024*1024, 1*1024*1024) - 若坚持用
rate包,必须配合ReserveN按 chunk 大小预占(如每次读 64KB 就ReserveN(now, 64*1024)),且必须立刻Wait() - 大文件上传务必加
io.Pipe:限速 Reader → goroutine → PipeWriter → HTTP Body,否则 multipart 场景下会被 client 多次Read,速率翻倍或卡死
真正容易被忽略的是:单机 rate.Limiter 在多实例部署时完全失效。3 个 Pod 各跑一个限流器,用户发 30 QPS 就绕过 10 QPS 限制——这时必须切到 Redis + Lua 或服务网格层,而不是强行给每个实例加权重或本地计数。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











