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 和进度统计共享同一读流。
示例逻辑:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
fileSize
-
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 可能被重复访问,导致覆盖
进度条后端真正的难点不在读取,而在状态同步的边界控制:什么时候算“开始”,什么时候算“结束”,中间断连怎么续。这些都得靠客户端配合做幂等和重试,后端只管好自己的那一段字节流。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










