teereader 不能直接统计读取字节数,因其设计仅转发数据且不记录累计量;必须通过自定义 io.writer(如 counterwriter)配合 multiwriter 或封装 progressreader 才能准确计数。

为什么 TeeReader 不能直接用来统计读取字节数
TeeReader 的设计目标是「把读到的数据同时写进另一个 Writer」,它本身不记录、不暴露已读字节数。你调用 Read() 时,它只返回本次读取的 n,不会累加或上报总进度。常见错误是以为包装后就能直接查「当前读了多少」,结果发现没接口、没字段、也没回调。
真正能拿到进度的方式,得靠你自己在 Writer 侧做计量——也就是把 TeeReader 的「副输出」做成一个计数器。
- 必须自己实现一个满足
io.Writer接口的类型,内部维护int64计数器 - 这个
Writer的Write()方法只负责累加长度,返回len(p)(不能返回 0 或错误,否则TeeReader会中断) - 不要试图从
TeeReader本身取值,它没有公开字段或方法暴露累计量
用 io.MultiWriter + 自定义计数 Writer 实现可靠流量统计
单独用 TeeReader 太单薄,实际场景往往还要把数据转发给别的地方(比如日志、缓存、加密器)。这时候推荐组合 io.MultiWriter 和自定义计数器,既清晰又可复用。
示例中 counterWriter 只做一件事:每次 Write() 就把长度加到 total 字段里。把它和真实目标 dst 一起塞进 MultiWriter,再喂给 TeeReader:
立即学习“go语言免费学习笔记(深入)”;
type counterWriter struct {
total int64
}
func (c *counterWriter) Write(p []byte) (n int, err error) {
return len(p), nil // 忽略写入失败,只计数
}
func (c *counterWriter) Total() int64 { return c.total }
// 使用:
cw := &counterWriter{}
mr := io.MultiWriter(cw, dst) // dst 是你真正的写入目标(如文件、网络连接)
tr := io.TeeReader(src, mr)
// 后续读取 tr,cw.Total() 就是已读总字节数
- 注意
Write()必须返回len(p),返回其他值会导致TeeReader提前退出或 panic - 如果
dst是可能出错的 Writer(如网络连接),MultiWriter会传播第一个错误;但计数器不能因此失败,所以它的Write()要忽略错误 - 并发读取时,
total字段需加sync/atomic保护,否则读写竞争会导致统计不准
TeeReader 在 HTTP 上传/下载中监控进度的典型陷阱
HTTP 场景下,TeeReader 常被用在 http.Request.Body 或 http.Response.Body 上。但这里有几个硬限制容易踩坑:
-
Request.Body默认是不可重读的,一旦被TeeReader包装并读过一次,后续中间件或 handler 再读就会得到空内容——除非你提前用httputil.DumpRequest或io.Copy(ioutil.Discard, ...)把原始 body 缓存下来 -
Response.Body如果来自http.Transport,底层可能是带缓冲的连接;强制用TeeReader中间截流可能导致连接复用失效,甚至触发服务端超时(尤其大文件下载) - 别在
http.HandlerFunc里直接对r.Body做io.TeeReader包装后传给下游库(如json.NewDecoder),因为下游可能调用Read()多次,而你的计数器只会按「每次写入长度」累加,不是按「逻辑单元」(比如一个 JSON 对象)
替代方案:什么时候该放弃 TeeReader 改用 io.ReadCloser 包装
如果你的真实需求不只是「边读边记数」,而是要「控制读取节奏」「支持取消」「需要精确到 chunk 级别的回调」,那 TeeReader 就太轻了。它只是一个被动管道,没法拦截、没法暂停、也没法注入上下文。
更可控的做法是自己封装一个 io.ReadCloser,在 Read() 里手动调用源 Read(),然后更新计数、触发回调、检查 ctx.Done():
type ProgressReader struct {
r io.Reader
total *int64
mu sync.Mutex
}
func (pr *ProgressReader) Read(p []byte) (n int, err error) {
n, err = pr.r.Read(p)
pr.mu.Lock()
*pr.total += int64(n)
pr.mu.Unlock()
return n, err
}
- 这种写法比
TeeReader多几行代码,但你能完全掌控读行为,比如在读满 1MB 后 sleep 10ms 控制速率 - 如果上游 Reader 本身不支持
io.Seeker(比如管道、网络流),就别幻想「回退」或「跳转」,所有进度都只能是单向累计 - 最易被忽略的一点:
TeeReader的副写入路径(Writer)如果阻塞,整个Read()就会卡住——而自定义ReadCloser可以把计数逻辑做成非阻塞(比如用 channel 异步上报)










