限速上传必须用io.Pipe+goroutine隔离状态,因HTTP客户端会多次调用Read导致令牌重复扣除、速率失控;io.LimitReader仅限总字节数,与时间无关,无法实现“字节/秒”级精确限速。

限速上传必须用 rate.Limiter 控制每次 Read 的字节数,且必须配合 io.Pipe + goroutine;直接把限速 Reader 当作 http.Request.Body 传入,会导致 HTTP 客户端多次调用 Read,速率翻倍甚至卡死。
为什么不能直接用 io.LimitReader 包裹文件
io.LimitReader 只限制总字节数,不是速率。比如 io.LimitReader(file, 1024*1024) 表示“最多读 1MB 后返回 io.EOF”,和时间无关。上传中途就断掉,客户端看到的是不完整数据或连接重置。
- 常见错误现象:
multipart.Writer反复调用Read,限速逻辑被重复触发 - 浏览器/客户端显示 “upload stalled” 或卡在 99%
- 服务端日志里
Read调用次数远超预期,CPU 却不高(大量阻塞等待)
上传限速必须加 io.Pipe + goroutine
HTTP 客户端(包括 http.Client 和 multipart.Writer)内部会做预读、缓冲、重试,它可能对同一个 io.Reader 多次调用 Read。限速 Reader 没有状态隔离,就会反复扣令牌,导致实际速率失控。
- 正确做法:启动一个 goroutine,从限速 Reader 读,往
io.PipeWriter写;http.Request.Body用io.PipeReader -
io.PipeWriter.Close()必须由写端显式调用,否则读端永远阻塞 - 大文件(>100MB)不加管道,
multipart内部 buffer 可能吃光内存 - 别忘了 recover panic 后也
close(writer),避免连接泄漏
rate.Limiter 的 ReserveN 必须配对 Wait
只调 limiter.ReserveN(ctx, n) 返回一个 *rate.Reservation,这仅是预检,不阻塞也不消耗令牌。漏掉 res.Wait(ctx),限速就完全失效。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 每次
Read前都要:先res := limiter.ReserveN(ctx, n),再立刻res.Wait(ctx) - 初始化 burst 至少等于 limit(例如 1MB/s →
burst = 1024 * 1024),否则 panic 或拒绝合法重试 - 别用
rate.Every(time.Second / bps),Go 编译器可能优化掉除法;应写rate.Limit(1024 * 1024) - ctx 被 cancel 时
Wait返回 error,此时应立即停止 goroutine 并 closeio.PipeWriter
上传场景下限速对象必须是「读取侧」
限速只能插在上传端读取原始数据的路径上,而不是写入网络或封装 multipart 的环节——后者不可控,且容易被 client 内部逻辑绕过。
- 如果你用
multipart.Writer,限速 Reader 应传给part.Write()的参数,不是整个file - 不要试图包装
http.Request.Body的WriteTo或拦截net.Conn.Write,这些层不暴露稳定接口,且破坏流控语义 - 别在 handler 里对
req.Body做io.Copy+io.LimitReader,那是下载逻辑,不是上传
真正难的不是写限速代码,而是让限速 Reader 在 HTTP client 多次 Read 下保持单次、原子、可取消的行为——这必须靠 io.Pipe 隔离状态,靠 ReserveN+Wait 保证令牌扣减时机精准。漏掉任一环,上传看起来“慢了”,但实际速率早已失真。

















