progressreader必须包装原始文件流而非直接包装os.file或http.request.body,因sdk内部多次read、seek或重试会导致读取错位或eof提前;正确做法是每次上传新建os.file或先完整读取request.body到内存/临时文件再包装。

ProgressReader 必须包装原始文件流,不能包装 *os.File 或 http.Request.Body 直接传入
Go 的对象存储 SDK(如阿里云 OSS、MinIO)上传接口要求传入 io.Reader,但它们内部会多次调用 Read 方法,甚至可能 seek 或重试。如果直接把 *os.File 或 http.Request.Body 包进 ProgressReader 后复用,会导致读取错位或 EOF 提前触发。
正确做法是:每次上传都新建一个 *os.File,用 os.Open 打开,并立即包进 ProgressReader;若来自 HTTP 请求,则需先将 Request.Body 完整读到内存或临时文件再包装——否则后续 multipart 解析或 SDK 内部重试会失败。
-
ProgressReader的file字段应为io.Reader类型,不是具体实现类(如*os.File),方便适配不同来源 - 回调函数里别做阻塞操作(如写日志到磁盘、发 HTTP 请求),否则拖慢上传速度
- 注意
Read方法可能返回n == 0 && err == nil(空读),此时不应更新进度
阿里云 OSS SDK 的 PutObjectFromFile 不走 ProgressReader,必须用 PutObject + 自定义 Reader
很多人误以为 PutObjectFromFile 支持进度回调,其实它底层是先读文件到内存再一次性上传,不经过用户可控的 Read 流程,所以无法注入 ProgressReader。真正能监控进度的只有 PutObject 接口,它接受 io.Reader 参数。
示例中常见错误是传入 os.Open("x.jpg") 而不包装,结果进度始终为 0 —— 因为没触发回调;或者传了包装后的 Reader,但忘了设置 total(文件大小),导致百分比计算失效。
- 调用
PutObject前,务必用os.Stat获取文件Size()并赋给ProgressReader.total - OSS 默认超时极短,大文件上传易卡在
PutObject返回前,必须显式加oss.Timeout(120 * time.Second) - 若用
resumableUpload模块,它内部会分片并多次调用Read,此时ProgressReader仍有效,但总进度需按分片累加,不能只看单次Read
MinIO 客户端上传进度监控和 OSS 几乎一致,但 multipart 上传需额外处理 boundary
MinIO 兼容 S3 API,PutObject 行为与 OSS 一致,同样依赖 io.Reader 注入进度逻辑。但如果你走的是 PutObjectMultipart(比如大文件自动分片),SDK 会自己构造 multipart/form-data 请求体,这时你无法直接控制 Body 的读取过程 —— 进度监控就得退回到服务端解析阶段,或改用客户端手动分片 + 单个 PutObject 调用。
另一个坑是:MinIO 的 Go SDK 对 Content-Length 更敏感。如果 ProgressReader.total 和实际文件大小不符,上传可能中途被拒绝,报错类似 SignatureDoesNotMatch 或 InvalidRequestBody。
- 用
minio.PutObjectOptions时,不要设ContentType为"multipart/form-data"—— 这是表单上传用的,对象直传应设为文件真实 MIME 类型(如"image/jpeg") - MinIO 的
PutObject不支持断点续传,大文件必须自己切片 +StartGetObject/PutObjectPart配合ProgressReader分段监控 - 本地调试时,MinIO server 日志默认不打印上传字节数,需加
--console-address :9001并查 Console UI 的 network tab 确认是否真在发数据
上传完成 ≠ 进度回调结束,要检查 err 是否为 io.EOF
很多实现把 update 回调放在 Read 方法末尾,但最后一次 Read 可能返回 n > 0 && err == io.EOF,也可能返回 n == 0 && err == io.EOF。前者是正常结束,后者可能意味着流提前中断 —— 比如网络断开、文件被删、或 SDK 内部重试失败。
更隐蔽的问题是:某些 SDK(尤其是封装了缓冲的)会在最后一次 Read 后多调一次 Read 来确认 EOF,这时 pr.read 已等于 pr.total,但回调又触发一次 100%,造成重复日志或 UI 跳变。
- 建议在
ProgressReader.Read中加判断:if err == io.EOF && n == 0 { return },避免空读触发回调 - 上传结束后,务必检查 SDK 返回的
err—— 即使进度显示 100%,也可能是上传失败后才返回错误 - 不要依赖进度回调次数判断完成,而应以 SDK 方法返回值为准;回调只是“尽力而为”的提示,不是原子性保证
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











