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

Go io.TeeReader 怎样边读边计算内容摘要

来源:17golang原创

时间:2026-10-17 22:31:28 248浏览 收藏

如果想在读取大文件、上传请求或消息流的同时计算 SHA-256,通常不需要先把全部内容读进内存。把摘要对象作为 io.Writer 交给 io.TeeReader,再让下游持续消费包装后的 Reader,就能在同一条读取链上完成“读取 + 摘要写入”。

官方资料:https://pkg.go.dev/io#TeeReader

关键边界只有一个:io.TeeReader 没有内部缓冲,写入摘要目标必须先完成,外层读取才会返回;如果下游只读了一部分,摘要也只覆盖已经读过的部分。

先把读取流和摘要写成同一条链

io.TeeReader(r, w) 返回一个新的 io.Reader。调用方从这个新 Reader 读取多少字节,TeeReader 就尝试把同一批字节写给 w。因此摘要对象可以直接使用 hash.Hash,不需要为摘要再开一个完整字节切片。

Go io.TeeReader 把源 Reader、读取缓冲区和 hash.Hash 摘要目标连接起来的静态结构图
图1:这张静态说明图展示源 Reader、io.TeeReader 与 hash.Hash 的关系;TeeReader 不保存额外完整副本,读取数据会同步写入摘要目标。

下面的组合适合“只关心摘要,不需要保留完整正文”的场景。io.Copy 会持续消费 TeeReader,sha256.New 则作为同步写入目标。

package main

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

func main() {
	// 源数据可以来自文件、HTTP 响应体或消息流。
	source := strings.NewReader("streamed payload")
	sum := sha256.New()

	// 每次读取的数据都会同步写入 sum,不先构造完整副本。
	tee := io.TeeReader(source, sum)
	written, err := io.Copy(io.Discard, tee)
	if err != nil {
		// 读取或摘要写入失败都要由调用方处理。
		panic(err)
	}

	// Copy 成功后再取结果,避免拿到只覆盖前半段输入的摘要。
	fingerprint := sum.Sum(nil)
	fmt.Printf("written=%d sha256=%x\n", written, fingerprint)
}

这里的 io.Discard 只是表示业务不需要保留正文;如果还要把数据写入文件或网络连接,可以把它替换成真正的目标 Writer。摘要对象本身实现了 io.Writer,所以不会改变调用方的读取接口。

读取完成后再取摘要

hash.Hash.Sum 会把当前状态追加到传入的切片并返回结果,它不会替你继续读取源数据。换句话说,sum.Sum(nil) 只是“截至目前已经写入摘要对象的内容”,不是对原始 Reader 的自动收尾。

Go io.TeeReader 完整消费、io.Copy 返回值、写入错误和 hash.Sum 结果的静态关系图
图2:这张静态说明图把完整消费边界、io.Copy 返回值、摘要写入错误和 hash.Sum(nil) 放在同一关系图中,帮助判断摘要是否覆盖完整输入。

如果下游要保存正文和摘要,可以把目标文件作为复制目标,摘要对象仍放在 TeeReader 后面:

package main

import (
	"crypto/sha256"
	"fmt"
	"io"
	"os"
)

func copyWithDigest(srcPath, dstPath string) ([]byte, int64, error) {
	// 打开源文件后,函数退出时释放文件描述符。
	src, err := os.Open(srcPath)
	if err != nil {
		return nil, 0, err
	}
	defer src.Close()

	// 创建目标文件;真实项目中还应按业务需要设置权限和覆盖策略。
	dst, err := os.Create(dstPath)
	if err != nil {
		return nil, 0, err
	}
	defer dst.Close()

	sum := sha256.New()
	tee := io.TeeReader(src, sum)
	written, err := io.Copy(dst, tee)
	if err != nil {
		// 源读取错误和摘要写入错误都会通过 Copy 返回。
		return nil, written, err
	}

	// Copy 成功意味着源数据已经被完整消费,摘要此时才可作为完整结果。
	return sum.Sum(nil), written, nil
}

func printDigest(path string) {
	digest, written, err := copyWithDigest("input.bin", path)
	if err != nil {
		fmt.Println("copy failed:", err)
		return
	}
	fmt.Printf("written=%d digest=%x\n", written, digest)
}

示例中的返回值同时保留了 written 和错误。发生失败时不要只看摘要,因为摘要可能只覆盖了失败前已经传过的字节;是否删除不完整目标文件,应由业务的重试和幂等策略决定。

摘要写入失败时,错误会从哪里返回

官方接口约定是:TeeReader 写入目标时遇到错误,会把这个错误报告成外层 Reader 的读取错误。使用 io.Copy 时,调用方通常从第二个返回值拿到第一个错误,同时用第一个返回值判断已经复制了多少字节。

这个行为也解释了为什么 TeeReader 不是后台异步镜像器。摘要 Writer 很慢,读取端就会被同步拖慢;摘要 Writer 返回错误,读取链就会停止。它适合把摘要、审计计数或落盘写入绑定到同一条数据流,但不适合把耗时很长的副作用偷偷放到读取热路径中。

实际项目里不必为了包装而定义额外类型,直接传入 hash.Hash 或实现了 io.Writer 的业务组件即可。需要特别注意短写入:Writer 返回的字节数少于输入长度且没有明确错误时,调用方应让底层 Writer 遵守 Go 的 Writer 约定,不要用 TeeReader 掩盖实现缺陷。

TeeReader、MultiWriter 和 io.ReadAll 怎么选

场景更合适的组合原因
边读边生成摘要,同时把数据交给下游io.TeeReader + io.Copy摘要写入跟随读取发生,不需要完整缓存输入。
同一份输入要同时写到多个输出io.MultiWriter它是一个 Writer,适合把一次写入复制到多个目标。
数据很小,后续需要多次遍历或随机访问io.ReadAll 后自行处理保留字节切片更直接,但内存成本由输入大小决定。

不要把 TeeReader 当作“读取器的可回放副本”。它只转发当前读取动作,源 Reader 仍然是一次性消费模型。如果需要重试上传或多次计算,应该把数据落盘、使用可重复读取的数据源,或明确设计缓存生命周期。

常见问题

不调用 Read,能不能直接得到完整摘要?

不能。摘要 Writer 只有在 TeeReader 实际读取数据时才会收到字节,必须让下游完整消费包装后的 Reader。

摘要对象可以和目标文件并行写入吗?

可以通过 TeeReader 让摘要写入跟随复制发生,但这是同步关系,不是两个独立后台任务。摘要写入错误会影响外层读取结果。

为什么得到的摘要和文件不一致?

先检查摘要对象是否被复用、是否只读取了一部分,以及摘要计算和文件写入是否使用了同一个 TeeReader。成功调用 io.Copy 后再调用 Sum(nil),边界最清楚。

io.TeeReader 会不会自动关闭源文件?

不会。它只实现 Reader 包装,不负责关闭底层资源。打开的文件、HTTP 响应体或连接仍要由拥有它们的代码显式关闭。

一句话总结:把 hash.Hash 当作 TeeReader 的写入端,把真正的目标当作下游消费端,并以 io.Copy 成功返回作为“摘要覆盖完整输入”的边界。

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