当前位置:首页 > 文章列表 > Golang > Go教程 > Go 怎么用 io.Reader 统计上传进度而不复制文件

Go 怎么用 io.Reader 统计上传进度而不复制文件

来源:17golang原创 2026-09-07 00:48:45 0浏览 收藏

上传一个大文件时,进度条不应该先把文件读成 []byte 再计算。更合适的做法是包一层 io.Reader:每次底层 Reader 返回 n 个字节,就把这个数量累加起来,同时原样把数据交给 HTTP 客户端或上传 SDK。这样内存仍按流式缓冲工作,进度值也能被单独读取。

要点速览
  • 进度统计放在 Reader 包装器,统计的是已经被上传调用方读取的字节数。
  • 不要用 io.ReadAllbytes.Buffer 复制整文件;包装器只转发同一批读取缓冲。
  • 上传失败后应重新打开文件、重新创建包装器;并发显示进度时用原子计数器。

上传进度应该统计哪一层

先把“上传了多少”拆成三个概念:本地文件被 Reader 读出了多少,客户端向网络写出了多少,以及服务端最终接收并落盘了多少。io.Reader 包装器只能可靠回答第一个问题,但这正是大多数客户端进度条需要的观察点。它不会把尚未读出的文件提前算进进度,也不会冒充服务端已经提交成功。

边界能说明什么不能说明什么
Reader 返回的 n上传调用方已经拿到的字节数服务端是否收完、是否落盘
HTTP 客户端写请求体客户端发送路径正在消耗数据服务端业务是否接受
服务端响应服务端对本次请求的业务结果客户端此前每一段的实时进度
Go io.Reader 上传进度图,展示本地文件、ProgressReader、已读字节计数、HTTP 上传调用方和服务端接收层的边界关系
图1:把进度观察点放在 Reader 包装层,避免把已读字节误称为服务端已落盘字节。

官方 io.Copy 的语义也是从 Reader 读到 EOF 或错误为止,并返回实际复制的字节数。因此,下面的计数器只围绕 Read 的返回值工作,不需要另建一份文件副本。

先把 io.Reader 包成可观测对象

最小实现只保存源 Reader 和一个累计值。注意要先累加 n 再返回:某些 Reader 可能在返回部分数据的同时返回错误,前面那部分已经交给调用方,不能漏计。

package progress

import (
    "io"
    "sync/atomic"
)

type Reader struct {
    source io.Reader
    read   atomic.Int64
}

func NewReader(source io.Reader) *Reader {
    return &Reader{source: source}
}

func (r *Reader) Read(p []byte) (int, error) {
    n, err := r.source.Read(p)
    // n 表示本次已经交给上传方的数据,哪怕 err 同时出现也要累计。
    r.read.Add(int64(n))
    return n, err
}

func (r *Reader) BytesRead() int64 {
    // 进度显示线程只读取快照,不修改 Reader 状态。
    return r.read.Load()
}

这个类型没有缓存文件内容,也没有改变 Read 的错误语义。它的计数单位是字节,返回值可以直接和 os.File.Stat 得到的文件大小比较。若源数据是压缩流、加密流或服务端会再次编码,分母应改成对应传输层的总长度,不能机械使用原文件大小。

把包装器接到上传请求并控制更新频率

上传请求通常会在 client.Do 内持续读取请求体,所以进度读取要放在另一个观察路径。下面的示例让上传 goroutine 消耗包装器,主流程每 200 毫秒读取一次原子快照;进度刷新频率不影响文件读取速度。

file, err := os.Open("backup.tar"); if err != nil {
    return err
}
defer file.Close()

info, err := file.Stat(); if err != nil {
    return err
}

tracked := progress.NewReader(file)
req, err := http.NewRequest(http.MethodPut, uploadURL, tracked)
if err != nil {
    return err
}
req.ContentLength = info.Size()

result := make(chan error, 1)
go func() {
    // 上传 goroutine 只负责让 HTTP 客户端持续读取请求体。
    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }
    result 

示例中的 ContentLength 只适合请求体长度就是本地文件长度的场景。若上传 SDK 使用 multipart、压缩或分块编码,长度可能未知或大于文件大小,此时显示“已读字节/总字节”前要先确认分母的含义。若 SDK 内部会重试,单个 Reader 是否可重放也要看它的接口约定。

Go ProgressReader 并发进度图,展示 source Reader、atomic.Int64、HTTP 请求体、上传 goroutine 和进度轮询器的静态关系
图2:ProgressReader 一条路径继续提供请求体,另一条路径只读取原子累计值用于进度显示。

失败时怎么回退,哪些数字不能继续用

上传返回错误后,当前计数只能说明请求体读到了哪里,不能当成下次重试的起点。安全的回退路径是关闭旧文件,重新打开源文件,重新创建 progress.Reader,再创建新的请求。不要复用已经读过一部分的文件句柄,也不要把旧计数直接展示成新请求的进度。

  • 部分读取和错误:先累计 n,再向上返回 err;调用方决定是否终止。
  • 未知总大小:显示已读字节和传输速率,不显示伪造的百分比。
  • 并发读取:如果上传和进度展示不在同一 goroutine,累计值必须用原子操作或互斥保护。
  • 服务端失败:收到 4xx/5xx 后把本次进度标记为失败,不能因为 Reader 已读完就显示成功。

上线前的检查清单

检查项通过条件
内存代码没有 io.ReadAll、整文件 bytes.Buffer 或无界缓存。
计数累计的是每次 Read 返回的 n,且 n 与错误同时出现时不丢失。
总量文件大小、multipart 长度和压缩后长度没有混为同一个分母。
重试失败后重新打开源文件、重新创建 Reader 和请求。
结果只有服务端成功响应才把进度状态改成完成。

相关问答

包装 io.Reader 会不会复制文件?

不会。包装器只保存源 Reader 和计数值,数据仍由调用方提供的读取缓冲流过;网络栈和 HTTP 客户端可能有自己的缓冲,但不会因为这个计数器而把整文件读入内存。

为什么不用 io.TeeReader 统计进度?

io.TeeReader 适合把读取到的数据同步写给另一个 Writer,但进度统计只需要保存一个整数。自定义包装器更容易暴露 BytesRead,也不会引入一个只为计数而存在的 Writer。

读取到文件末尾就代表上传成功了吗?

不代表。它只表示上传调用方已经消费完 Reader;网络中断、服务端校验失败或业务提交失败仍可能发生。最终状态必须以客户端收到的成功响应为准。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
PHP 8.4 属性钩子怎么避免把校验逻辑写散PHP 8.4 属性钩子怎么避免把校验逻辑写散
上一篇
PHP 8.4 属性钩子怎么避免把校验逻辑写散
Java 虚拟线程执行阻塞 IO 时怎么设置并发边界
下一篇
Java 虚拟线程执行阻塞 IO 时怎么设置并发边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    167次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    93次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    15次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    28次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    61次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码