io.TeeReader核心逻辑是每次Read时将读取数据原样写入指定io.Writer(如*os.File),不创建/打开/关闭文件,需用户预置可写文件句柄并处理错误;写入失败会立即返回error,且全程同步阻塞。

io.TeeReader 读取时写入文件的核心逻辑
io.TeeReader 不会自动创建或打开文件,它只是在每次 Read 调用时,把读到的数据「顺便」写进你给的 io.Writer。所以必须自己准备好一个已打开的 *os.File(或其它 io.Writer),并确保它支持写入。
常见错误是传入只读文件、未检查 os.OpenFile 返回的 error,或者误以为 io.TeeReader 会帮你关文件 —— 它完全不碰资源生命周期。
- 用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)获取可写文件句柄 - 务必检查返回的
error,尤其权限不足或路径不存在时会静默失败 -
io.TeeReader的第二个参数是io.Writer,不能传nil或只读对象(否则运行时报 panic:「write on closed file」或「bad file descriptor」)
典型用法:边读 HTTP 响应边保存到磁盘
这是最常被搜索的场景:下载大文件时不想全载入内存,又想同时存本地、校验哈希或转发给下游。
关键点在于:你得把 io.TeeReader 接在数据源之后、消费方之前;比如 http.Response.Body 是源,io.Copy 是消费者,中间插一个 io.TeeReader 就能分流写入。
resp, _ := http.Get("https://example.com/big.zip")
defer resp.Body.Close()
f, _ := os.OpenFile("big.zip", os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
// TeeReader 把 resp.Body 读出的数据,同时写进 f
tee := io.TeeReader(resp.Body, f)
// 后续所有从 tee 读取的动作,都会触发写入 f
_, _ = io.Copy(io.Discard, tee) // 丢弃内容,但已写入文件
- 注意顺序:
io.TeeReader必须在io.Copy或其它读操作之前构造,否则没机会拦截数据 - 如果后续还要读取原始内容(比如解析 JSON),不能直接用
tee多次Read—— 它不缓存,第二次读就从源头继续流,文件已写过一遍,但内存里没留副本 - 写入目标
f若中途出错(如磁盘满),io.TeeReader.Read会立即返回该 error,调用方需处理
为什么不能直接 tee 到 os.Stdout 或 log.Logger?
可以,但容易踩两个坑:一是控制台输出含不可见字符(如 ANSI 转义序列)导致文件损坏;二是 log.Logger 写的是带前缀和换行的格式化文本,不是原始字节流。
-
os.Stdout是合法io.Writer,但仅适合调试小数据;大流量下 stdout 缓冲策略可能拖慢整体读取 -
log.New(f, "", 0)创建的 logger 不是 raw writer,它会额外加时间戳、换行符,破坏二进制完整性 - 真正需要日志时,应该单独起 goroutine 监听进度(比如每 1MB 打一行 log),而不是用
io.TeeReader混合写入
替代方案对比:TeeReader vs MultiWriter vs 自己封装
如果你发现 io.TeeReader 不够用(比如要写多个文件、或需要异步写入避免阻塞读取),说明它已经超出设计边界。
-
io.MultiWriter是「一写多发」,但它是Write端分流,不是Read端分流 —— 你得自己控制读、再手动Write到多个目标 - 想并发写入(比如同时存 SSD 和上传 S3),别用
io.TeeReader,改用io.Pipe+ goroutine 分流更可控 - 最易忽略的一点:
io.TeeReader的写入是同步阻塞的,如果目标 writer(如网络存储)延迟高,整个读取会被拖慢;生产环境务必考虑超时或缓冲层
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











