http.Request.Body 不能直接读两次,因其是单次读取的 io.ReadCloser,首次读取后偏移量已达末尾,后续读取返回空;可用 io.TeeReader 或缓冲 body 解决,但需权衡内存与大文件风险。

为什么 http.Request.Body 不能直接读两次?
因为 http.Request.Body 是一个 io.ReadCloser,底层通常是单次读取的流(比如 socket buffer)。第一次读完后,内部 offset 已到末尾,再次 io.Copy 或 form.ParseMultipartForm 就会得到空内容——这是进度条卡在 0% 的最常见原因。
解决思路是用 io.TeeReader 或缓冲整个 body,但要注意内存和大文件风险:
- 小文件(bytes.Buffer 全量缓存,再分别解析表单和计算进度
- 大文件:必须流式处理,用
io.TeeReader+ 自定义io.Writer记录已读字节数 - 别直接对
r.Body调两次ParseMultipartForm,Go 标准库不会帮你 rewind
如何用 io.TeeReader 实现实时进度上报?
核心是把原始请求体包装成带“监听”的读取器,在每次读取时触发回调。关键不是拦截所有字节,而是让 multipart parser 和进度统计共享同一读流。
示例逻辑:
立即学习“go语言免费学习笔记(深入)”;
// 创建进度追踪 writer(仅计数,不写磁盘)
var totalRead int64
progressWriter := &ProgressWriter{onWrite: func(n int64) {
totalRead += n
// 推送到客户端:WebSocket / SSE / 或暂存到 map[requestID]int64
updateProgress(requestID, totalRead, fileSize)
}}
// 包装原始 Body
teeReader := io.TeeReader(r.Body, progressWriter)
// 用 teeReader 构造新 request(需替换 Body)
r2 := r.Clone(r.Context())
r2.Body = ioutil.NopCloser(teeReader)
// 正常解析 multipart
err := r2.ParseMultipartForm(32 << 20)
注意:fileSize 需提前从 Content-Length 获取(浏览器上传必带),否则无法算百分比;updateProgress 函数要线程安全,避免并发写冲突。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
multipart.Form.File 里的 Header 不含真实文件大小
multipart.FileHeader.Size 看起来像文件大小,但它只是 form boundary 解析出的声明值,可能被篡改或不准确(尤其分块上传场景)。真正可信的只有 Content-Length 请求头。
所以进度计算必须依赖:
-
r.Header.Get("Content-Length")→ 转成int64作为总大小 - 不要依赖
fileHeader.Size做进度分母,它只适合校验或预分配 - 如果前端用 fetch +
FormData,Content-Length一定存在;若用分块上传(如 tus),则需服务端维护 session 状态累加
并发上传时怎么避免进度数据错乱?
每个请求的进度必须隔离。常见错误是把 totalRead 定义成包级变量,或用简单 map 但没加锁。
推荐做法:
- 用请求唯一标识(如
uuid.NewString())作为 key,存到 sync.Map 或 Redis - 在 handler 开头生成 ID,通过
context.WithValue透传,避免闭包捕获问题 - 设置过期时间(如 10 分钟),防止内存泄漏;上传完成或超时后主动清理
- 别用
http.Request.URL.Query().Get("id")当进度 key——URL 可能被重复访问,导致覆盖
进度条后端真正的难点不在读取,而在状态同步的边界控制:什么时候算“开始”,什么时候算“结束”,中间断连怎么续。这些都得靠客户端配合做幂等和重试,后端只管好自己的那一段字节流。

















