rate.limiter基于令牌桶算法控制请求频次,需共享实例、配合context超时使用,并与transport连接池协同实现分层限流。

用 golang.org/x/time/rate 实现请求频次限流
限流的核心不是压网络带宽,而是控请求发起频率。Go 官方扩展库 rate.Limiter 是最轻量、最常用的选择,它基于令牌桶算法,适合控制 QPS(如每秒最多 10 次 HTTP 请求)。
常见错误是直接在 http.Client 层做连接复用控制,结果发现并发数降了但请求仍被后端拒绝——那是因为没真正限制「发出请求」的动作本身。
- 初始化一个每秒 5 个令牌、初始桶容量为 10 的限流器:
limiter := rate.NewLimiter(rate.Every(200 * time.Millisecond), 10) - 每次发请求前调用
limiter.Wait(ctx),它会阻塞直到拿到令牌;超时则返回 error - 不要在每个 goroutine 里新建
Limiter,必须共享同一个实例,否则完全失效 - 若需区分用户/租户限流,得按 key 维护 map of
*rate.Limiter,并加锁或用sync.Map
用 net/http.Transport 控制并发连接数
频次限流解决“太频繁”,而连接数限制解决“太并发”。默认 http.DefaultTransport 允许每 host 最多 100 个空闲连接,容易打爆目标服务或耗尽本地 fd。
典型现象:大量 context deadline exceeded 或 dial tcp: lookup failed,但 CPU 和带宽都空闲——其实是连接建立阶段被系统或对方拒绝了。
立即学习“go语言免费学习笔记(深入)”;
- 设置全局最大空闲连接:
transport.MaxIdleConns = 20 - 限制每 host 并发请求数:
transport.MaxConnsPerHost = 5 - 缩短空闲连接存活时间避免堆积:
transport.IdleConnTimeout = 30 * time.Second - 务必显式复用 transport:
client := &http.Client{Transport: transport},别用http.Get这类快捷函数
带宽限制不能靠纯 Go 标准库硬控
Go 的 net/http 没有内置带宽节流(如限速 1MB/s)。你无法通过修改 request body 或 response reader 来“让数据流得慢一点”——TCP 层不认这个逻辑,强行 sleep 只会让连接超时、重试增多。
真实场景中,带宽控制通常发生在更底层或外部:
- Linux 下用
tc命令对网卡限速(如tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms),Go 程序无感知但整体出口带宽被压住 - 如果必须从 Go 内部软限,只能对读取 response body 做节流(例如用
io.LimitReader+ 定时器),但这只影响消费速度,不减少发送量,也不缓解服务端压力 - 上传大文件时,可分块 +
time.Sleep控制 chunk 发送间隔,但要注意Request.Body必须支持多次 Read,且服务器要接受长连接和慢上传
组合使用时注意顺序和作用域
限频、限连、限带宽三者不是叠加关系,而是分层生效:先过连接池(transport),再过频次(limiter),最后才到数据流(bandwidth)。最容易忽略的是 context 生命周期和错误传播。
-
limiter.Wait(ctx)的 ctx 应该带 timeout,否则一个卡住的令牌会拖垮整个 goroutine 池 - transport 的
Response.Body一定要 close,否则连接无法归还,MaxConnsPerHost形同虚设 - 不要在 limiter 外围包一层 “if rand.Float64()
- 压测时观察
rate.Limiter.Reserve()返回的OK和Delay,比单纯看错误率更能反映限流是否起效
真正难的不是写几行限流代码,而是判断该在哪一层限、对谁限、失败后怎么退避——这些得结合监控指标(如后端 5xx、P99 延迟、连接建立耗时)来动态调整,而不是写死参数。










