限速不是截断,而是单位时间读取字节数;正确做法是用 rate.Limiter 实现 io.ReadCloser,每次 Read 后调 WaitN(n),Close 透传 resp.Body.Close(),并分层配置超时。

限速不是截断:别直接用 io.LimitReader 包裹整个响应体
直接写 io.LimitReader(resp.Body, 1024*1024) 只会读完 1MB 就返回 io.EOF,这不是限速,是硬截断。HTTP 下载中途断掉,文件损坏,客户端收不到完整响应。限速的核心是「单位时间读多少字节」,而不是「最多读多少字节」。
常见错误现象:io.Copy(dst, io.LimitReader(resp.Body, n)) 看似在控制,实则只是提前终止流;后续 resp.Body.Close() 可能漏调,导致连接泄漏;更糟的是,某些代理或 CDN 会因响应未读完而重传或复位连接。
- 正确做法:把
resp.Body传给一个真正按时间节奏读的io.Reader实现,比如封装rate.Limiter - 必须保持
io.ReadCloser接口:自定义 Reader 要同时实现Read([]byte) (int, error)和Close() error,且Close()内必须调用原始resp.Body.Close() - 别省略超时配置:
http.Transport.ResponseHeaderTimeout必须设(建议 ≤10s),否则服务端迟迟不发 header,client.Do()就卡死,根本到不了限速那步
用 golang.org/x/time/rate.Limiter 做字节级令牌桶限速
这是目前最稳定、线程安全、压测友好的方案。关键在于:每次 Read 返回 n 字节后,立刻用 limiter.WaitN(ctx, n) 消耗 n 个令牌——每个令牌 = 1 字节,速率就精确对应「字节/秒」。
示例初始化限速器(512KB/s):limiter := rate.NewLimiter(rate.Limit(512*1024), int(512*1024))
注意:burst 参数必须 ≥ limit,否则 panic;设太小(如 burst=1)会让正常重试失败,设太大(如 burst=1000000)等于没限速。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 别写
rate.Every(time.Second / 512000)—— Go 编译器可能优化掉除法,应显式用rate.Limit(512*1024) - 每次
Read后必须调WaitN,只调ReserveN不等待,限速逻辑完全失效 - 如果上传或下载中途
ctx被 cancel,WaitN返回 error,此时应立即停止读取并 clean up(如 closeio.PipeWriter)
上传限速必须加 io.Pipe + goroutine
直接把限速 Reader 当作 http.Request.Body 传入,HTTP client 可能多次调用 Read(尤其在 multipart 场景下),导致限速被重复触发、速率翻倍,甚至卡死。根本原因是 client 内部做了 buffer 预读或重试,而限速 Reader 没状态隔离。
正确结构是:
启动一个 goroutine,从限速 Reader 读 → 往 io.PipeWriter 写;
HTTP 请求体用 io.PipeReader;io.PipeWriter.Close() 必须由写端触发,否则读端永远阻塞。
- 大文件(>100MB)不用
io.Pipe,内存占用会飙升——client 或multipart.Writer的内部缓冲会把整块数据吃进内存 -
io.Pipe没有超时机制,务必配合context.WithTimeout控制整体上传生命周期 - 别忽略 recover:goroutine 内 panic 后记得
defer pipeWriter.Close(),否则连接泄漏
别依赖 http.Client.Timeout 控制限速阶段
http.Client.Timeout 是全局超时,覆盖 dial、header、body 全流程。限速下载本就该花时间读 body,设个 30s 全局 timeout,很可能在读到一半时被误杀,报 context.DeadlineExceeded,但实际只是还没读完。
应该分层设超时:
— http.Transport.ResponseHeaderTimeout:控制 header 接收时间(≤10s)
— http.Transport.IdleConnTimeout:避免空闲连接拖慢复用
— http.Transport.TLSHandshakeTimeout:防止握手阶段卡死
— body 读取阶段靠限速 Reader 自身节奏,不额外加 timeout
- 限速 Reader 的
Read方法里不能自己 sleep 或 wait 超过 10s,否则单次读就可能触发上层 context deadline - 如果服务端响应极慢(如磁盘 IO 阻塞),靠限速无法解决,得结合
context.WithTimeout包裹整个下载流程,而非仅限速逻辑
io.ReadCloser,且 Close() 里要透传关闭原始 body——漏掉这一句,连接不会释放,跑几天后 fd 耗尽,服务就静默挂了。

















