当前位置:首页 > 文章列表 > Golang > Go问答 > Go bufio.Writer.ReadFrom 复制大文件时怎样处理短写

Go bufio.Writer.ReadFrom 复制大文件时怎样处理短写

来源:17golang原创 2026-10-13 04:38:09 0浏览 收藏

用 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。
bufio.Writer.ReadFrom 在快速路径和缓冲路径之间的关系结构图
图1:结构说明图,ReadFrom 会先处理已有缓冲,再在底层 ReaderFrom 快速路径与普通缓冲读取路径之间选择。

因此,同一段业务代码在写入 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 返回值、Flush 短写和错误出口的关系图
图2:结果说明图,真正的底层短写在 Flush 处形成错误,调用方应从 ReadFrom 与 Flush 两个出口收集证据。

可以按下面的顺序定位:

  1. 先看 ReadFrom 的 n。如果 n 小于预期且同时有源 Reader 错误,优先检查源文件读取、网络输入或上游解压过程。
  2. 再看 ReadFrom 的 err。普通 EOF 会被当作完成处理;io.ErrNoProgress 则说明 Reader 连续返回空结果,没有推进。
  3. 最后单独看 Flush。如果 ReadFrom 为 nil 而 Flush 返回 io.ErrShortWrite 或底层错误,问题发生在最终写出,而不是源数据读取。
  4. 不要用“目标文件存在”作成功条件。文件已经创建,只能说明打开或创建动作完成,不能证明全部字节写完。

如果 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。把两个错误出口分别记录即可缩小范围。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go big.Int.SetString 使用 base 0 时为什么前缀会改变进制Go big.Int.SetString 使用 base 0 时为什么前缀会改变进制
上一篇
Go big.Int.SetString 使用 base 0 时为什么前缀会改变进制
甲壳虫ADB助手搜索不到电视或车机怎么办?同网、ADB与授权排查
下一篇
甲壳虫ADB助手搜索不到电视或车机怎么办?同网、ADB与授权排查
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    448次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    519次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    535次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    480次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    304次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码