Go io.TeeReader 怎样统计流经读取器的字节数
在上传、解压或解析大文件时,常见需求不是把整个输入读进内存,而是想知道“已经流经读取链多少字节”。Go 的 io.TeeReader 很适合做这件事:把原始 io.Reader 包装起来,每次读取到的数据同步写入一个统计用 io.Writer,主流程仍然从包装后的 Reader 读取。
官方文档:https://pkg.go.dev/io#TeeReader
核心规则是“读多少,旁路 Writer 就收到多少”。TeeReader 没有内部缓冲,旁路写入完成前本次读取不会完成;如果旁路写入报错,这个错误会通过 Read 返回。
先把读取链变成可计数的 Reader
io.TeeReader(r, w) 返回的仍然是一个 io.Reader。它不会主动启动协程,也不会预读;只有调用包装器的 Read 时,才先从 r 取数据,再把本次取到的有效字节交给 w。因此可以直接把它传给原来接收 Reader 的函数。

package main
import (
"fmt"
"io"
"strings"
)
func main() {
input := strings.NewReader("alpha-beta-gamma")
var count countingWriter
// 包装后仍实现 io.Reader,原有的 io.Copy 等调用可以继续复用。
tee := io.TeeReader(input, &count)
if _, err := io.Copy(io.Discard, tee); err != nil {
// 旁路 Writer 的错误会从 tee 的 Read 链路返回。
panic(err)
}
fmt.Println(count.n)
}
type countingWriter struct {
n int64
}
func (w *countingWriter) Write(p []byte) (int, error) {
// 这里只累计本次收到的长度,不保留数据副本。
w.n += int64(len(p))
return len(p), nil
}
这个计数器的优点是内存开销与输入大小无关。它只保存一个整数,读完后即可得到旁路收到的总字节数。若主流程提前停止,计数值也只代表“已经通过 TeeReader 的字节”,不是原始资源的声明大小。
计数 Writer 之外,还能接入什么旁路
统计只是最小用法。只要对象实现 io.Writer,就能把同一批有效字节交给它。例如 hash.Hash 可以持续计算摘要,bytes.Buffer 可以保留小输入副本,自定义 Writer 则可以把累计值投递给进度回调。选择旁路对象时,要先明确是否需要保存全部内容。

package stream
import (
"crypto/sha256"
"encoding/hex"
"io"
)
func digestAndCount(src io.Reader) (int64, string, error) {
hashWriter := sha256.New()
count := &countingWriter{}
// 先统计,再把同一批读取结果交给摘要 Writer;不需要保存完整正文。
tee := io.TeeReader(src, io.MultiWriter(count, hashWriter))
if _, err := io.Copy(io.Discard, tee); err != nil {
// 读取错误或任一旁路写入错误都必须交给调用方处理。
return count.n, "", err
}
return count.n, hex.EncodeToString(hashWriter.Sum(nil)), nil
}
type countingWriter struct{ n int64 }
func (w *countingWriter) Write(p []byte) (int, error) {
// 返回完整长度,才能让 MultiWriter 把这次写入视为成功。
w.n += int64(len(p))
return len(p), nil
}
这里的 io.MultiWriter 只是把一次旁路写入分发给两个 Writer;文章的核心仍是 TeeReader 负责把“读取动作”连接到“旁路写入”。如果摘要 Writer 或计数 Writer 返回错误,调用方应停止继续消费并保留已返回的错误。
最容易误判的三个边界
- 没有内部缓冲:TeeReader 不会把数据暂存后异步写出。旁路 Writer 较慢时,主读取也会等待,不能把它当作后台进度通道。
- 按有效字节计数:Writer 收到的是本次 Read 返回的
p[:n],不是缓冲区容量。读取返回n > 0且同时带错误时,先处理这n个字节,再处理错误。 - 写入错误会中断读取语义:旁路写失败后,TeeReader 从 Read 返回写入错误。计数值可能已经增加,不能仅凭计数值就判断整个输入成功完成。
如果只想显示进度,可以让 Writer 只更新原子计数或发送受控事件;如果需要摘要,不要另起一条读取原始 Reader 的链路,否则两条读取会争抢同一数据源。对网络请求体、文件上传这类一次性流,TeeReader 的“边读边旁路”通常比先 ReadAll 更稳妥。
常见问题
Q:TeeReader 会把数据缓存起来吗?
不会。它没有内部缓冲,旁路 Writer 的写入必须在本次 Read 返回前完成。
Q:为什么统计值小于文件大小?
它统计的是已经被调用方读取并通过旁路的字节。如果上层提前停止、读取报错或只读取了限制范围,统计值自然可能小于资源总大小。
Q:旁路 Writer 返回短写怎么办?
Writer 应在返回少于输入长度时同时返回非空错误;不要把短写当作成功,并让错误回到调用方。
Go slog.NewJSONHandler 怎样输出结构化 JSON 日志
- 上一篇
- Go slog.NewJSONHandler 怎样输出结构化 JSON 日志
- 下一篇
- Go http.Transport.ReadBufferSize 怎样调整 HTTP 客户端读缓冲区
-
- Golang · Go教程 | 12分钟前 |
- Go Zap 怎样把不同等级日志分流到两个文件
- 213浏览 收藏
-
- Golang · Go教程 | 13分钟前 | 日志 · zap · go · Go ZAP 日志等级 AtomicLevel
- Go Zap 怎样通过 AtomicLevel 动态调整日志等级
- 433浏览 收藏
-
- Golang · Go教程 | 13分钟前 |
- Go Zap 怎样用 ObjectMarshaler 输出结构化对象
- 380浏览 收藏
-
- Golang · Go教程 | 17分钟前 |
- Go Zerolog 怎样用 Hook 统一添加日志等级字段
- 475浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- Go fsnotify 怎样合并短时间重复的写入事件
- 280浏览 收藏
-
- Golang · Go教程 | 20分钟前 |
- Go Zerolog 怎样为子日志器绑定固定业务字段
- 316浏览 收藏
-
- Golang · Go教程 | 24分钟前 |
- Go fsnotify 怎样给新建子目录补充监听
- 375浏览 收藏
-
- Golang · Go教程 | 28分钟前 | go ·
- Go Cobra 怎样给位置参数设置数量范围
- 138浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Go fsnotify 怎样同时消费文件事件与错误通道
- 452浏览 收藏
-
- Golang · Go教程 | 29分钟前 |
- Go Cobra 怎样为子命令继承全局选项
- 272浏览 收藏
-
- Golang · Go教程 | 30分钟前 |
- Go Cobra 怎样要求两个参数必须同时出现
- 284浏览 收藏
-
- Golang · Go教程 | 1小时前 | 日志 · 标准库 · go · 工程实践 · Go 结构化日志 JSON日志 log/slog HandlerOptions slog.NewJSONHandler
- Go slog.NewJSONHandler 怎样输出结构化 JSON 日志
- 440浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 769次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 766次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 847次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 817次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 646次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览

