http 请求体 body 是单次读取流,无法重复读取,故上传大文件时需边读边计数;推荐用 io.teereader + 自定义计数 writer 实现无侵入实时进度追踪。

为什么 http.Request.Body 无法重复读取会导致进度丢失
上传大文件时想实时上报进度,第一反应是读一遍 Request.Body 计算总长、再读一遍转发——但 Go 的 http.Request.Body 是单次读取流,第二次 Read 会直接返回 io.EOF。这不是 bug,是设计使然:底层用的是 io.ReadCloser,没有重置能力。
真正可行的路径只有一条:在读取过程中边读边计数,而不是事后统计。这意味着必须把原始 Body 包装成一个可“观测”的读取器。
实操建议:
- 不要尝试调用
r.Body.Close()后再重新赋值r.Body,HTTP handler 已持有引用,改了也没用 - 避免用
io.Copy直接转发,它不暴露中间字节数;改用io.CopyN分块或自定义io.Reader - 优先使用
io.TeeReader+ 自定义io.Writer组合,轻量且符合 Go 的组合哲学
用 io.TeeReader + 闭包计数器实现无侵入进度追踪
io.TeeReader 会在每次 Read 时把数据写入指定的 io.Writer,而我们只需要一个空的、只计数的 Writer,就能在不改变业务逻辑的前提下拿到实时字节数。
示例片段(用于 handler 内):
var uploaded int64
progressWriter := io.WriterFunc(func(p []byte) (int, error) {
uploaded += int64(len(p))
// 这里可以发 WebSocket / channel / log 等
return len(p), nil
})
reader := io.TeeReader(r.Body, progressWriter)
// 后续用 reader 替代 r.Body 做解析或存储
err := r.ParseMultipartForm(32
<p>关键点:</p>
-
ParseMultipartForm必须在包装后调用,否则它会直接消费原始Body,导致TeeReader拿不到数据 - 后续读取具体文件(如
file.Open()返回的multipart.File)仍是独立流,不影响进度计数 - 如果用了
io.MultiReader或其他组合读取器,确保TeeReader处于最外层
前端配合:为什么不能只靠 Content-Length
很多人试图从 r.Header.Get("Content-Length") 获取总大小来算百分比——这在普通 POST 中可能有效,但在 multipart/form-data 中完全不可靠。因为该 header 表示整个请求体长度(含 boundary、字段名、换行符等),不是文件净尺寸,误差常达几 KB 到几十 KB,且无法在服务端拆分出单个文件长度。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
更糟的是:部分代理或客户端(如某些 iOS WebView)会省略或伪造 Content-Length,导致除零或负数计算。
正确做法:
- 前端用
XMLHttpRequest.upload.onprogress或fetch+ReadableStream获取真实上传字节数(event.loaded) - 服务端只负责“接收多少”,不负责“应有多少”;前后端进度逻辑解耦
- 若必须服务端校验总大小,应在所有文件接收完毕后,用
os.Stat或写入时累计值比对,而非依赖 header
并发上传多个文件时的进度隔离怎么做
一个请求带多个 <input type="file" multiple> 文件时,multipart.Form.File 是 map,但所有文件共享同一个 Request.Body 流。此时 io.TeeReader 的计数器是全局的,无法区分哪个文件传到哪了。
解决思路不是给每个文件单独包装,而是按「字段名」做路由:
- 前端为每个文件字段加唯一 name,如
file-1、file-2 - 服务端在
ParseMultipartForm后,遍历form.File,对每个file.Header记录其Filename和预期大小(前端可通过额外字段传) - 用
sync.Map或map[string]*int64+sync.RWMutex存各文件已接收字节数,key 为字段名 - 在
TeeReader的Write闭包里,根据当前解析到的字段动态更新对应计数器(需结合multipart.Reader手动解析,或改用mime/multipart底层解析)
这一步明显变重,多数场景下推荐前端自行管理多文件进度,服务端只做单流吞吐监控——除非有强审计需求,否则不值得为隔离进度引入完整 multipart 解析逻辑。
真正容易被忽略的是:进度回调的频率。高频更新(如每字节都发)会拖垮 WebSocket 或日志系统。建议按固定块(如 64KB)或时间窗口(如 200ms 内合并一次)做节流,而不是裸奔式上报。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










