Go io.TeeReader 怎么在读取时同步保存原始数据
当一份数据只以 io.Reader 形式到达,而程序既要解析它,又要同步保存原始字节时,可以把源 Reader 和保存目标 Writer 交给 io.TeeReader。下游每读取一段,TeeReader 就把同一段写入目标;它不会预读整份数据,也不会在内部缓存完整副本。
官方文档:https://pkg.go.dev/io#TeeReader
TeeReader 是读取适配器,不是后台复制器
我第一次用 TeeReader,是在接收一段请求体时既要反序列化 JSON,又要留一份原始载荷。它最吸引人的地方不是“同时做两件事”,而是保持原来的读取接口:业务代码仍然拿到一个 io.Reader,保存动作被包在读取过程里。
这个模式有三个关键约束:
- 只有通过 TeeReader 读出的字节,才会写入副本。
- 写入是同步的,目标 Writer 完成写入后,本次 Read 才会返回。
- 目标 Writer 的错误会作为读取错误返回给下游。

最小写法:读取多少,副本里就有多少
先用内存 Writer 看清最基本的行为。这里故意只读取前 5 个字节,副本也只会得到这 5 个字节。
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
src := strings.NewReader("hello, tee reader")
var saved bytes.Buffer
// tee 仍然是 Reader;读取时会把相同字节同步写入 saved。
tee := io.TeeReader(src, &saved)
buf := make([]byte, 5)
// 先处理 n 个有效字节,再检查 err,符合 io.Reader 的通用约定。
n, err := tee.Read(buf)
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("read=%q saved=%q\n", buf[:n], saved.Bytes())
}
这段代码不会把剩余内容自动补进 saved。如果业务读取器提前结束,副本也会停在同一位置。这个特性既节省内存,也决定了“保存完整原始数据”必须确保最终消费到 EOF。
实际接入:解析 JSON 时先写临时文件
在文件落盘场景里,我更愿意先写临时文件,等读取、解析和关闭都成功后再改名。这样失败时不会留下一个看起来像完整文件的半成品。
package archive
import (
"encoding/json"
"fmt"
"io"
"os"
)
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
}
func DecodeAndSave(src io.Reader, finalPath string) (Event, error) {
var event Event
tmp, err := os.CreateTemp("", "event-*.json")
if err != nil {
return event, fmt.Errorf("创建临时文件: %w", err)
}
tmpPath := tmp.Name()
// 只有最终提交成功才保留文件,其他路径统一清理临时数据。
committed := false
defer func() {
if !committed {
_ = os.Remove(tmpPath)
}
}()
tee := io.TeeReader(src, tmp)
decoder := json.NewDecoder(tee)
// 解码器通过 tee 读取的每个字节都会同步进入临时文件。
if err := decoder.Decode(&event); err != nil {
_ = tmp.Close()
return event, fmt.Errorf("解析 JSON: %w", err)
}
// 继续读到 EOF,确保解码器未消费的空白或尾部也进入原始副本。
if _, err := io.Copy(io.Discard, tee); err != nil {
_ = tmp.Close()
return event, fmt.Errorf("保存剩余原始数据: %w", err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return event, fmt.Errorf("同步临时文件: %w", err)
}
if err := tmp.Close(); err != nil {
return event, fmt.Errorf("关闭临时文件: %w", err)
}
if err := os.Rename(tmpPath, finalPath); err != nil {
return event, fmt.Errorf("提交原始文件: %w", err)
}
committed = true
return event, nil
}
这个实现强调的是保存边界,而不是宣称 Rename 在所有文件系统和跨目录场景都具有相同语义。生产代码还应保证临时文件与目标文件位于合适的目录,并按业务需要处理权限、覆盖策略和目录同步。
最容易忽略的是“下游读完没有”
很多解析器只读取完成当前任务所需的部分。JSON Decoder 解出一个对象后,流中可能还剩空白、第二个对象或其他尾部。若函数此时返回,TeeReader 不会替你继续读取,保存文件自然不完整。
解决方式取决于业务目标:
- 只想保存“业务实际消费的字节”,不要额外 drain,副本天然就是消费范围。
- 必须保存完整输入,在业务处理后继续从 TeeReader 读到 EOF。
- 还要拒绝多余 JSON 值,应继续用 Decoder 检查尾部,而不是简单丢弃后续数据。
这也是 TeeReader 与“先完整落盘,再从文件解析”的主要取舍:前者延迟低、占用小,但副本完整性与消费行为绑定;后者多一次存储路径,却更容易把完整落盘设为业务前置条件。

写入错误为什么会表现成读取错误
TeeReader 对外只暴露 Reader,所以目标 Writer 出错时,下游看到的是 Read 返回错误。官方语义还说明它没有内部缓冲:写入必须完成,本次读取才能完成。对我来说,这既是优点也是代价。
- 优点:保存失败不会悄悄被忽略,业务读取可以及时停止。
- 代价:慢磁盘、慢网络 Writer 会直接拉低读取吞吐。
- 边界:若 Writer 接受的字节数少于要求且没有返回明确错误,读取链路会收到短写错误。
因此,不要把一个不可控的远程上传 Writer 直接接在关键解析链路上,除非你就是希望保存端的背压约束读取端。若两边必须独立调度,可以考虑队列、临时文件或 io.Pipe 配合 goroutine,但那会引入缓冲上限、取消、关闭顺序和错误汇聚等新问题。
TeeReader、MultiWriter 和 ReadAll 怎么选
| 方案 | 适合场景 | 主要代价 |
|---|---|---|
io.TeeReader | 一个输入被业务读取,同时复制到一个 Writer | 副本完整性依赖下游是否读到 EOF,Writer 会同步阻塞读取 |
io.MultiWriter | 调用方主动写入一份数据,并同步写给多个 Writer | 它解决的是“一写多份”,不是包装 Reader |
io.ReadAll 后分别处理 | 数据较小,需要多次随机使用完整字节 | 整份数据进入内存,大输入不合适 |
| 先完整落盘再解析 | 完整副本是强前置条件,允许增加一次存储路径 | 延迟和磁盘 I/O 更明显 |
我的采用判断清单
我会在下面几项都成立时优先用 TeeReader:输入天然是流;业务只需顺序读取;副本 Writer 足够可靠;保存失败应当让读取失败;并且代码能明确决定是否读到 EOF。
如果原始数据必须无条件完整保存、保存端速度不可控,或者多个消费者要独立读取,我通常不会只靠 TeeReader。它最适合的是“同一读取动作顺带复制”,而不是替代消息队列、异步归档或多消费者广播。
相关问题
TeeReader 会把整份数据放进内存吗?
不会。它没有内部整份缓存,只对每次读出的字节执行对应写入。
解析失败后,临时文件一定包含完整原文吗?
不一定。解析失败发生前读到多少,就只写入多少;是否继续读取并保存剩余数据,需要由业务明确决定。
可以把哈希计算器作为 Writer 吗?
可以。许多哈希对象实现 io.Writer,因此可在读取时同步累计摘要;但摘要只覆盖实际经过 TeeReader 的字节。
繁花动漫阅读界面怎么调?亮度、横竖屏与夜间模式说明
- 上一篇
- 繁花动漫阅读界面怎么调?亮度、横竖屏与夜间模式说明
- 下一篇
- 诗歌本有哪些诗歌资源?多套歌本目录与更新说明
-
- Golang · Go教程 | 9分钟前 |
- Go fstest.MapFS 怎么测试依赖 fs.FS 的组件
- 370浏览 收藏
-
- Golang · Go教程 | 2小时前 | 流式处理 · Go教程 · Go io.Pipe CloseWithError PipeWriter
- Go io.PipeWriter CloseWithError 怎么把失败传给读端
- 478浏览 收藏
-
- Golang · Go教程 | 3小时前 | flag · Go教程 · flag 命令行参数 FlagSet VisitAll Go flag.Visit
- Go flag.Visit 怎么区分用户显式传入的参数
- 168浏览 收藏
-
- Golang · Go教程 | 4小时前 | 标准库 · go · flag TextVar TextUnmarshaler
- Go flag.TextVar 怎么复用 encoding.TextUnmarshaler
- 394浏览 收藏
-
- Golang · Go教程 | 5小时前 |
- Go 自定义错误怎么实现 Is 方法匹配错误族
- 239浏览 收藏
-
- Golang · Go教程 | 5小时前 |
- Go errors.As 怎么匹配实现接口的错误类型
- 213浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Go errors.Join 怎么合并清理阶段的多个错误
- 240浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 244次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 290次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 259次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 240次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 49次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- 快速掌握Go语言HTTP标准库的实现方法
- 2022-12-30 327浏览
-
- Go语言中的IO操作及Flag包的用法
- 2022-12-30 491浏览

