Go bufio.Writer.ReadFrom 复制大文件时怎样处理短写
用 bufio.Writer.ReadFrom 复制大文件时,先不要把所有“读到的数据少于缓冲区”都当成短写。Reader 一次返回较少字节属于正常的短读,ReadFrom 会继续读取;真正需要重点检查的是底层 Writer 在 Flush 时是否只接收了一部分数据,以及调用方有没有检查最后一次 Flush 的错误。
稳妥的判断顺序是:先处理 ReadFrom 返回值,再处理 Flush 返回值,最后用返回的字节数和错误类型定位问题。官方文档:https://pkg.go.dev/bufio#Writer.ReadFrom
先判断 ReadFrom 实际走哪条路径
bufio.Writer.ReadFrom 实现了 io.ReaderFrom。它会先检查自己的错误状态,然后判断底层 Writer 是否也实现了 io.ReaderFrom:
- 底层 Writer 支持
io.ReaderFrom,并且缓冲区为空时,调用会把读取工作交给底层的快速路径。 - 如果
bufio.Writer中已经有数据,先把这部分数据写出去,再决定是否委托底层 Writer。 - 没有快速路径时,
ReadFrom直接把 Reader 读入自己的缓冲区,读满后再由Flush写给底层 Writer。

因此,同一段业务代码在写入 bytes.Buffer、普通文件或自定义 Writer 时,底层调用次数可能不同。不要把某一种底层 Writer 的调用次数当成 ReadFrom 的通用保证;应该只依赖它返回的字节数与错误。
把短读、短写和 EOF 分成三类
排查“复制后文件变小”时,建议把现象分为三层:
| 现象 | 通常代表什么 | 应该检查什么 |
|---|---|---|
Reader 返回 m > 0,但 m 小于缓冲区剩余容量 | 短读,可能仍有后续数据 | 不要提前结束循环,继续交给 ReadFrom |
Reader 返回 0, nil 很多次 | 输入源没有推进 | 达到连续空读上限时关注 io.ErrNoProgress |
| 底层 Writer 写入字节少于请求且没有错误 | 真正的底层短写 | Flush 会把它转换为 io.ErrShortWrite |
| ReadFrom 成功但 Flush 失败 | 数据仍在 bufio 缓冲区,最终写出阶段失败 | 把 Flush 错误当成复制失败处理 |
特别容易误判的是 EOF。源 Reader 返回 EOF 时,ReadFrom 会把 EOF 转成成功结束;如果缓冲区恰好填满,它可能主动 Flush,否则尾部数据仍可能留在 bufio.Writer 内。因此“ReadFrom 返回 nil”并不等于“底层文件已经接收全部数据”。
最小可用写法:ReadFrom 和 Flush 都要检查
对大文件复制,调用方可以保持固定的错误出口。下面的示例不把整个文件读进内存,也不把短读误判为失败:
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func copyLargeFile(dstPath, srcPath string) error {
// 源文件只负责提供字节,关闭动作放在打开成功之后注册。
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("打开源文件: %w", err)
}
defer src.Close()
// 创建目标文件后交给 bufio.Writer,避免一次性分配大文件大小的内存。
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("创建目标文件: %w", err)
}
defer dst.Close()
writer := bufio.NewWriterSize(dst, 256*1024)
written, err := writer.ReadFrom(src)
if err != nil {
return fmt.Errorf("ReadFrom 复制 %d 字节后失败: %w", written, err)
}
// ReadFrom 结束时仍可能有尾部缓冲,Flush 是最终写出检查点。
if err := writer.Flush(); err != nil {
return fmt.Errorf("Flush 写出 %d 字节后失败: %w", written, err)
}
return nil
}
func main() {
// 真实项目中应把路径来自参数或配置,并在调用处记录错误。
if err := copyLargeFile("backup.bin", "source.bin"); err != nil {
fmt.Println(err)
}
}
这里的 written 是 ReadFrom 已经从 Reader 消费并计数的字节数,不要把它直接当作“磁盘已经持久化的字节数”。只有 ReadFrom 和 Flush 都成功返回,代码才完成了这一层 I/O 的写出流程。
遇到短写时,先看错误发生在哪一层
底层 Writer 的 Write 如果返回的数量小于请求数量且错误为 nil,bufio.Writer.Flush 会把结果转换为 io.ErrShortWrite,并保留尚未写出的那部分缓冲与错误状态。之后继续调用同一个 Writer,通常只会得到同一个错误,不会自动恢复。

可以按下面的顺序定位:
- 先看 ReadFrom 的 n。如果 n 小于预期且同时有源 Reader 错误,优先检查源文件读取、网络输入或上游解压过程。
- 再看 ReadFrom 的 err。普通 EOF 会被当作完成处理;
io.ErrNoProgress则说明 Reader 连续返回空结果,没有推进。 - 最后单独看 Flush。如果 ReadFrom 为 nil 而 Flush 返回
io.ErrShortWrite或底层错误,问题发生在最终写出,而不是源数据读取。 - 不要用“目标文件存在”作成功条件。文件已经创建,只能说明打开或创建动作完成,不能证明全部字节写完。
如果 Writer 是网络连接、限流写入器或自定义存储适配器,还要检查它是否遵守 io.Writer 契约:返回的字节数不能超过输入长度;返回少量字节时应同时返回错误,或者让上层得到 io.ErrShortWrite。不符合契约的 Writer 会让任何缓冲策略都难以可靠判断。
反向验证:用结果和清单确认复制没有静默失败
正式任务中可以把成功条件写成一份很短的清单:
- 源文件打开成功,且关闭错误按项目需要记录。
ReadFrom返回的错误为 nil,并记录返回字节数。- 显式调用
Flush,并把它的错误视为最终写出结果。 - 目标文件关闭或同步动作的错误按存储介质要求处理。
- 需要更强一致性时,再用文件大小、校验和或业务记录做独立比对;不要只看进度日志。
结论可以压缩成一句话:ReadFrom 负责把 Reader 的数据搬进缓冲写入链路,短读通常会继续,短写最终要在 Flush 处暴露。复制大文件时,永远同时检查 ReadFrom 和 Flush,并把错误发生的层级记录下来。
相关问题
ReadFrom 返回 nil 后还需要 Flush 吗?
需要。除非你明确知道底层快速路径已经完成全部写出,否则应显式 Flush;这样才能处理仍在 bufio 缓冲区中的尾部数据。
Reader 每次只返回几 KB 是短写吗?
不是。这属于 Reader 的短读,只要返回了正数并且后续仍能推进,ReadFrom 会继续读取。
为什么复制数量看起来正确,目标内容仍不完整?
常见原因是最后一次 Flush 失败,或者调用方只记录了 ReadFrom 的结果而忽略了 Flush。把两个错误出口分别记录即可缩小范围。
Go big.Int.SetString 使用 base 0 时为什么前缀会改变进制
- 上一篇
- Go big.Int.SetString 使用 base 0 时为什么前缀会改变进制
- 下一篇
- 甲壳虫ADB助手搜索不到电视或车机怎么办?同网、ADB与授权排查
-
- Golang · Go问答 | 11分钟前 |
- Go http.Server.ConnContext 怎么为每条连接附加自定义元数据
- 313浏览 收藏
-
- Golang · Go问答 | 12分钟前 |
- Go http.Server.ErrorLog 怎样接管服务器内部错误输出
- 151浏览 收藏
-
- Golang · Go问答 | 16分钟前 |
- Go httptest.NewRequestWithContext 怎样测试请求上下文取消
- 230浏览 收藏
-
- Golang · Go问答 | 20分钟前 |
- Go testing/quick.Check 失败后怎样复现同一组输入
- 352浏览 收藏
-
- Golang · Go问答 | 21分钟前 |
- Go testing/quick.Value 为什么会拒绝含未导出字段的结构体
- 236浏览 收藏
-
- Golang · Go问答 | 23分钟前 | go · Go GOOS go/build Context.MatchFile 文件名约束
- Go go/build.MatchFile 为什么会受 GOOS 和文件名约束影响
- 261浏览 收藏
-
- Golang · Go问答 | 25分钟前 |
- Go ast.FilterFile 过滤声明时为什么会删除导入
- 417浏览 收藏
-
- Golang · Go问答 | 44分钟前 |
- Go go/doc.ToText 如何把注释格式化成终端文本
- 250浏览 收藏
-
- Golang · Go问答 | 47分钟前 | 标准库 · go · Go go/doc.Filter 文档树 未导出声明 公开API
- Go go/doc.Filter 怎样从文档树中移除未导出声明
- 178浏览 收藏
-
- Golang · Go问答 | 50分钟前 | go · Go runtime/coverage WriteCountersDir 覆盖率计数文件
- Go runtime/coverage.WriteCountersDir 生成的计数文件如何配合分析
- 341浏览 收藏
-
- Golang · Go问答 | 53分钟前 | 并发 · 读写锁 · go · Go sync.RWMutex tryLock 非阻塞写入
- Go sync.RWMutex.TryLock 怎样实现非阻塞写入尝试
- 360浏览 收藏
-
- Golang · Go问答 | 54分钟前 | go · Go runtime/coverage WriteMetaDir 覆盖率元数据
- Go runtime/coverage.WriteMetaDir 怎样保存原生覆盖率元数据
- 180浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 448次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 519次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 535次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 480次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 304次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go代码规范错误处理示例经验总结
- 2022-12-23 278浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览

