Go 怎么用 io.Reader 统计上传进度而不复制文件
上传一个大文件时,进度条不应该先把文件读成 []byte 再计算。更合适的做法是包一层 io.Reader:每次底层 Reader 返回 n 个字节,就把这个数量累加起来,同时原样把数据交给 HTTP 客户端或上传 SDK。这样内存仍按流式缓冲工作,进度值也能被单独读取。
- 进度统计放在 Reader 包装器,统计的是已经被上传调用方读取的字节数。
- 不要用
io.ReadAll或bytes.Buffer复制整文件;包装器只转发同一批读取缓冲。 - 上传失败后应重新打开文件、重新创建包装器;并发显示进度时用原子计数器。
上传进度应该统计哪一层
先把“上传了多少”拆成三个概念:本地文件被 Reader 读出了多少,客户端向网络写出了多少,以及服务端最终接收并落盘了多少。io.Reader 包装器只能可靠回答第一个问题,但这正是大多数客户端进度条需要的观察点。它不会把尚未读出的文件提前算进进度,也不会冒充服务端已经提交成功。
| 边界 | 能说明什么 | 不能说明什么 |
|---|---|---|
| Reader 返回的 n | 上传调用方已经拿到的字节数 | 服务端是否收完、是否落盘 |
| HTTP 客户端写请求体 | 客户端发送路径正在消耗数据 | 服务端业务是否接受 |
| 服务端响应 | 服务端对本次请求的业务结果 | 客户端此前每一段的实时进度 |

官方 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 是否可重放也要看它的接口约定。

失败时怎么回退,哪些数字不能继续用
上传返回错误后,当前计数只能说明请求体读到了哪里,不能当成下次重试的起点。安全的回退路径是关闭旧文件,重新打开源文件,重新创建 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;网络中断、服务端校验失败或业务提交失败仍可能发生。最终状态必须以客户端收到的成功响应为准。
PHP 8.4 属性钩子怎么避免把校验逻辑写散
- 上一篇
- PHP 8.4 属性钩子怎么避免把校验逻辑写散
- 下一篇
- Java 虚拟线程执行阻塞 IO 时怎么设置并发边界
-
- Golang · Go教程 | 5分钟前 | 文件处理 · zip · Go教程 · Go archive/zip ZIP目录索引 FileHeader
- Go 怎么为 ZIP 文件生成目录索引而不解压内容
- 408浏览 收藏
-
- Golang · Go教程 | 28分钟前 | 文件处理 · go · archive/tar · 安全解包 · Go archive/tar tar.Reader 文件解包
- Go archive/tar 怎么保留目录结构解包到指定目录
- 136浏览 收藏
-
- Golang · Go教程 | 50分钟前 | 标准库 · Go教程 · IO处理 · Go 流式读取 io.Reader io.MultiReader
- Go 怎么把多个 Reader 拼成一个连续输入流
- 426浏览 收藏
-
- Golang · Go教程 | 1小时前 | go · bufio · 日志处理 · Go bufio.Scanner 日志读取
- Go bufio.Scanner 读取长日志怎么解除单行长度限制
- 140浏览 收藏
-
- Golang · Go教程 | 1小时前 | csv · Go教程 · 文件解析 · Go encoding/csv CSV读取
- Go 读取 CSV 时怎么保留字段中的换行和逗号
- 157浏览 收藏
-
- Golang · Go教程 | 1小时前 | 内存 · JSON · go · 性能 · Go encoding/json json.Decoder JSON流式解析
- Go 怎么流式解析超大 JSON 数组而不一次读入内存
- 160浏览 收藏
-
- Golang · Go教程 | 14小时前 |
- Go HTML 模板怎么复用页头页脚和基础布局
- 193浏览 收藏
-
- Golang · Go教程 | 15小时前 |
- Go 怎么解析源码并列出函数名和参数
- 119浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 167次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 93次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 15次使用
-
- LangGPT
- LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
- 28次使用
-
- ClickPrompt
- ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
- 61次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- Golang实现HTTP编程请求和响应
- 2022-12-28 101浏览
-
- golangNewRequest/gorequest实现http请求的示例代码
- 2023-01-24 343浏览

