progressreader 通过轻量包装器劫持 read 调用实现进度监控,无需预知文件大小,支持并发安全(需 mutex 同步)、回调非阻塞、total 动态适配(如 content-length 或 0),避免 stat.size() 误用及 multiwriter 语义混淆。

Go 里实现带进度的 io.Reader,核心不是改 io.Copy,而是用一个轻量包装器劫持每次 Read 调用——只要它能拿到原始数据流、累计字节数、触发回调,就足够了。它不依赖文件大小预知,也不要求服务端支持 Content-Length,适用于下载、上传、本地拷贝等所有基于 io.Reader 的场景。
ProgressReader 的 Read 方法必须原子更新计数
并发读取时(比如多个 goroutine 同时调用 Read),read 字段会竞争错乱。常见错误是直接写 pr.read += int64(n),没加锁或没用原子操作。
- 若只在单 goroutine 中使用(如配合
io.Copy),可省略同步逻辑,但代码要显式注明“not safe for concurrent use” - 若需多 goroutine 安全,推荐用
sync.Mutex包裹更新和回调(比atomic.AddInt64更易控制回调时机) - 回调函数(如打印、发 HTTP 状态)本身不能阻塞;否则会拖慢整个读取流程,尤其在小缓冲区(如默认 32KB)下,1GB 文件可能触发超 32K 次同步调用
不要用 os.Stat().Size() 当实时 total
很多初学者在构造 ProgressReader 时直接传 file.Stat().Size() 当 total,这在本地文件没问题,但一旦换成 http.Response.Body 或管道(io.Pipe),Size() 就是 -1 或不准——因为流长度根本不可知。
- 上传场景中,
total应来自os.File.Stat().Size()(仅限已知大小的本地文件) - 下载场景中,优先从
resp.Header.Get("Content-Length")解析;若为空,设为 0 并用“已读 / ?”格式显示(避免除零 panic) - 对 multipart 上传,
multipart.Writer写入前无法预知总长,此时应先用io.MultiWriter+io.TeeReader预扫描或接受动态 total
io.TeeReader 是更简洁的替代方案,但有隐含约束
io.TeeReader(r, w) 本质是把从 r 读出的数据,**同步写入 w**,再返回新 Reader。它天然适合“边读边记”的场景,比如写日志+计数。
- 它不暴露当前读了多少,得自己实现一个
io.Writer(如ProgressWriter)来接收“被 tee 出去”的数据并累加 - 写入
w的过程是阻塞的:如果w.Write慢(比如写磁盘卡顿),Read就会卡住——这点和自定义ProgressReader的 callback 一样,但更容易被忽略 - 它无法区分“读成功但写失败”的情况;错误会从
Write返回并终止后续Read,而自定义ProgressReader可选择忽略写失败只计数
上传进度必须绑定在 Request.Body 的读取路径上
HTTP 上传进度不是监听网络 socket,而是控制 Request.Body.Read 这一环。如果你把 ProgressReader 套在文件上,再塞进 multipart.Writer,那回调反映的就是“multipart 编码后写入 body 的字节数”,这才是真实上传量。
- 别在
http.Client.Do外围用time.Now()打点——网络延迟、TLS 握手、服务端排队都会让时间 ≠ 数据量 - 别用
io.MultiWriter(os.Stdout, file)混合进度和目标写入;MultiWriter是广播,不是代理,会导致回调重复且数值失真 - 大文件上传建议采样更新:比如每 1MB 触发一次回调,而不是每次
Read都调,避免 UI 频繁重绘或日志刷屏
最易被忽略的一点:ProgressReader 和 ProgressWriter 的语义完全不对称——前者监控“源流出多少”,后者监控“目标流入多少”。在网络传输中,它们可能差几百 KB(TCP 缓冲、TLS 加密开销、HTTP 分块头)。选哪一种,取决于你真正想回答的问题:“用户点了上传,现在发出去多少?”还是“服务器那边实际收到了多少?”
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











