限速下载需用自定义io.Reader封装响应体而非io.LimitReader,实现Read和Close方法,配合time.Sleep或ticker控制读取节奏,并设置ResponseHeaderTimeout等传输层超时避免卡死。

用 io.LimitReader 包裹响应体就能限速
限速下载的本质,是控制从 http.Response.Body 读取字节的速度。Go 标准库的 io.LimitReader 并不是为限速设计的——它只限制总字节数,但可以“骗过”它:每读一次就重建一个只允许读 n 字节的新 LimitReader,配合 time.Sleep 就能模拟恒定速率。
更靠谱的做法是用 io.LimitReader + time.Ticker 做令牌桶式节流,但最轻量、最常用且足够稳定的方案,其实是封装一层自定义 io.Reader,每次 Read 前等待对应时间。
- 别直接对整个响应体套
io.LimitReader(r.Body, maxBytes)—— 这只会截断下载,不是限速 - 正确做法:把
r.Body传给一个限速 Reader,例如newRateLimitedReader(r.Body, 512*1024)(512KB/s) - 注意
http.Client默认不设置超时,限速后单次读取可能卡住,务必配Timeout或Deadline
http.Transport 的 ResponseHeaderTimeout 会影响限速稳定性
限速下载时,如果服务端响应头迟迟不发完(比如 CDN 缓存未命中、后端慢),而你又没设好 transport 级超时,resp, err := client.Do(req) 可能卡死几十秒,根本到不了读 body 那步。
- 必须设置
http.Transport.ResponseHeaderTimeout,建议 ≤ 10s -
http.Transport.IdleConnTimeout和http.Transport.TLSHandshakeTimeout也建议显式设值,避免连接阶段拖慢整体节奏 - 不要依赖
http.Client.Timeout全局超时——它覆盖了 dial、header、body 全流程,限速时 body 阶段本就该花时间,全局 timeout 容易误杀
限速 Reader 实现里最容易漏掉 io.ReadCloser 接口
HTTP 响应体是 io.ReadCloser,你包装后也得保持这个接口,否则 defer resp.Body.Close() 会关错对象,导致连接泄漏或文件句柄耗尽。
立即学习“go语言免费学习笔记(深入)”;
- 自定义限速 Reader 必须同时实现
Read([]byte) (int, error)和Close() error -
Close()方法里要调用原始Body.Close(),不能只 close 自己的 ticker 或 buffer - 常见错误:返回
&limitReader{...}却没实现Close,结果resp.Body.Close()调的是nil或默认空方法 - 示例片段:
type rateLimitedReader struct { r io.ReadCloser rate int // bytes per second timer *time.Ticker } func (r *rateLimitedReader) Read(p []byte) (n int, err error) { wait := time.Second / time.Duration(r.rate) * time.Duration(len(p)) select { case <-r.timer.C: case <-time.After(wait): } return r.r.Read(p) } func (r *rateLimitedReader) Close() error { return r.r.Close() }
并发下载多个文件时,别给每个请求配独立限速器
如果你开 10 个 goroutine 同时下载,每个都按 1MB/s 限速,实际带宽会飙到 10MB/s——这违背“总速率限制”的初衷。
- 限速应作用于全局带宽池,而不是单请求。用一个共享的
token bucket或semaphore控制每秒总输出字节数 - 简单做法:用
golang.org/x/time/rate.Limiter,在每次Read后调用limiter.WaitN(ctx, n),让所有 goroutine 共享同一个 limiter 实例 - 注意
rate.Limiter的burst参数别设太大,否则短时突发会冲垮下游;建议burst = rate / 2(单位:字节) - 别在循环里反复
time.Sleep——精度差、无法取消、阻塞 goroutine;用limiter.WaitN可响应 context 取消
限速看似只是加个 sleep,但 HTTP 生命周期里的每个环节——连接建立、头解析、body 流式读取、连接复用、错误重试——都会被速率策略悄悄放大问题。真正稳的限速,永远是从 transport 配置开始,到 reader 接口守全,再到并发模型对齐业务目标。


















