登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go io.TeeReader 怎样同时读取和记录数据

来源:17golang原创

时间:2026-10-14 02:22:05 454浏览 收藏

处理上传、请求体或文件流时,经常既要把数据交给主解析器,又想顺手记录原始内容或计算摘要。直接对同一个 io.Reader 调用两次读取,第二次通常只能拿到 EOF;把全部内容先读进内存,又会失去流式处理的好处。io.TeeReader 的思路是把“记录”挂到主读取动作上:主流程每读到一段字节,TeeReader 就同步写给另一个 io.Writer。

要点速览
  • io.TeeReader(r, w) 返回的仍然是 Reader,只有下游真正读取时才会把相同字节写给 w。
  • 它没有内部缓冲,写入 w 必须完成;写入错误会被报告成读取错误。
  • 小体积审计可以用 bytes.Buffer,大流量更适合接摘要计算器、限长 Writer 或直接丢弃不需要的数据。

本文依据 Go 官方 io 包文档,围绕“只消费一次、同时记录数据”的架构取舍展开。

把 TeeReader 看成读取边界上的同步旁路

它不是复制出两个可以各自前进的 Reader,而是把原始 Reader 和记录 Writer 组合成一个新的 Reader。主流程读取新 Reader 时,底层数据先从 r 读出,再写入 w,最后返回给主流程。也就是说,记录动作和读取动作共享同一份字节,不需要让 r 再走一遍。

Go io.TeeReader 将原始 Reader、同步记录 Writer 与主读取方连接起来的结构说明图
图1:读取方、io.TeeReader 与记录 Writer 的静态关系说明图;这是原创结构图,不是运行截图。

这个模式适合“主线必须继续消费,旁路只观察或沉淀数据”的场景。例如把请求体交给 JSON 解码器,同时把字节送给摘要计算器;或者把文件流交给上传逻辑,同时记录少量审计信息。

用一次 ReadAll 同时拿到主结果和记录副本

下面的例子用 bytes.Buffer 保存小体积输入。注意真正触发复制的是对 tee 的读取,而不是创建 TeeReader 的那一行。

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	// source 只作为主输入被消费一次,audit 保存读取过程中经过的字节。
	source := strings.NewReader("user=alice&action=upload")
	var audit bytes.Buffer

	// tee 仍然实现 io.Reader;每次读取都会同步写入 audit。
	tee := io.TeeReader(source, &audit)
	body, err := io.ReadAll(tee)
	if err != nil {
		// 读取错误也可能来自 audit.Write,不能只检查源读取。
		log.Fatal(err)
	}

	fmt.Printf("主流程: %s\n", body)
	fmt.Printf("记录副本: %s\n", audit.String())
}

这里的两个字符串内容相同,但它们不是通过第二次读取 source 得到的。io.ReadAll 消费 tee,TeeReader 在每个 Read 调用里同步完成旁路写入。若记录器只需要计算摘要,可以把 &audit 换成 sha256.New(),避免保存整个原文。

真正的边界:没有缓冲,写入失败就是读取失败

官方文档明确说明 TeeReader 没有内部缓冲,写入动作必须在对应的 Read 返回前完成。因此记录 Writer 变慢时,主流程也会变慢;记录 Writer 返回错误时,主流程看到的是一个读取错误。这种设计让旁路失败不会被悄悄吞掉,但也意味着不能把高延迟网络 Writer 随便挂在热路径上。

Go io.TeeReader 的同步写入、错误传播和内存边界结构说明图
图2:TeeReader 的阻塞与错误传播边界说明图,展示记录 Writer 对主读取的影响;这是原创结构图。

生产代码通常先回答三个问题:记录数据是否必须完整保存?记录失败是否应阻止主流程?记录器会不会长期阻塞?如果只是审计摘要,使用 hash.Hash 更合适;如果只保留前几 KB,可以给 Writer 外面套一个限长实现;如果旁路完全不重要,才考虑显式忽略它的错误。

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"strings"
)

func main() {
	// hash.Hash 是 Writer,只保存固定大小的摘要,不保存整段输入。
	digest := sha256.New()
	tee := io.TeeReader(strings.NewReader("large stream body"), digest)

	// 主流程必须读完 tee,摘要才代表完整经过的字节。
	if _, err := io.Copy(io.Discard, tee); err != nil {
		panic(err)
	}
	fmt.Println(hex.EncodeToString(digest.Sum(nil)))
}

选择 TeeReader 前先看这张清单

需求更合适的做法需要留意
小请求体,需要保留原文bytes.Buffer + io.TeeReader限制请求体大小,避免无界增长
大文件,只需要校验摘要hash.Hash + io.TeeReader主流程必须读到结束,摘要才完整
需要把同一段数据发给多个 Writer考虑 io.MultiWriter任一 Writer 出错都会影响写入结果
旁路不能影响主流程单独的异步队列或明确的丢弃策略不要误以为 TeeReader 自带异步缓冲

最常见的误区是创建 TeeReader 后没有读取它,或者继续读取原始 source。前者不会产生任何记录,后者会绕过旁路。另一个误区是把 bytes.Buffer 用在不受控的大流量输入上;TeeReader 解决的是“单遍复制”,不负责替你设计内存上限、超时和背压策略。

常见问题

io.TeeReader 会不会自动把原始 Reader 读两遍?

不会。它只从原始 Reader 读一次,并把读到的字节同步写给 Writer;记录副本来自同一遍读取。

为什么 Writer 出错后主流程收到的是 Read 错误?

因为旁路写入和当前 Read 是一一对应的,写入没有完成就不能把这段数据视为成功读取,所以 TeeReader 将写入错误返回给读取方。

需要完整保存大文件时该怎么做?

不要默认使用 bytes.Buffer。优先保存摘要或限长片段;如果确实需要完整副本,应把存储容量、磁盘空间、取消信号和失败策略单独设计。

一句话总结:io.TeeReader 适合把轻量、同步、可解释的记录动作挂在一次读取上;它不会替你提供异步队列或无界缓存。先确定旁路的可靠性和容量边界,再决定 Writer 的实现。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>