为什么使用ioutil.ReadAll 函数需要注意
1. 引言
当我们需要将数据一次性加载到内存中,ioutil.ReadAll
函数是一个方便的选择,但是ioutil.ReadAll
的使用是需要注意的。
在这篇文章中,我们将首先对ioutil.ReadAll
函数进行基本介绍,之后会介绍其存在的问题,以及引起该问题的原因,最后给出了ioutil.ReadAll
函数的替代操作。通过这些内容,希望能帮助你更好地理解和使用ioutil.ReadAll
函数。
2. 基本说明
ioutil.ReadAll
其实是标准库的一个函数,其作用是从Reader
参数读取所有的数据,直到遇到EOF为止,函数定义如下:
func ReadAll(r io.Reader) ([]byte, error)
其中r
为待读取数据的Reader
,数据读取结果将以字节切片的形式来返回,如果读取过程中遇到了错误,也会返回对应的错误。
下面通过一个简单的示例,来简单说明ioutil.ReadAll
函数的使用:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
filePath := "example.txt"
// 打开文件
file, err := os.Open(filePath)
if err != nil {
fmt.Println("无法打开文件:%s", err)
return
}
defer file.Close()
// 读取文件全部数据
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Println("无法读取文件:%s", err)
return
}
// 将读取到的数据转换为字符串并输出
content := string(data)
fmt.Println("文件内容:")
fmt.Println(content)
}
在这个示例中,我们使用os.Open
函数打开指定路径的文件,获取到一个os.File
对象,接着,调用 ioutil.ReadAll
便能读取到文件的全部数据。
3. 为什么使用 ioutil.ReadAll 需要注意
从上面的基本说明我们可以得知,ioutil.ReadAll
的作用是读取指定数据源的全部数据,并将其以字节数组的形式来返回。比如,我们想要将整个文件的数据加载到内存中,此时就可以使用 ioutil.ReadAll
函数来实现。
那这里就有一个问题, 加载一份数据到内存中,会耗费多少内存资源呢? 按照我们的理解,正常是数据源数据有多大,就大概消耗多大的内存资源。
然而,如果使用 ioutil.ReadAll
函数加载数据时消耗的内存资源,可能与我们的想法存在一些差距。通常使用 ioutil.ReadAll
函数加载全部数据有可能会消耗更多的内存。
下面我们创建一个10M的文件,然后写一个基准测试函数,来展示使用 ioutil.ReadAll
加载整个文件的数据,需要分配多少内存,函数如下:
func BenchmarkReadAllMemoryUsage(b *testing.B) {
filePath := "largefile.txt"
for n := 0; n < b.N; n++ {
// 打开文件
file, err := os.Open(filePath)
if err != nil {
fmt.Println("无法打开文件:%r", err)
return
}
defer file.Close()
_, err = ioutil.ReadAll(file)
if err != nil {
b.Fatal(err)
}
}
}
基准测试的运行结果如下:
BenchmarkReadAllMemoryUsage-4 106 14385391 ns/op 52263424 B/op 42 allocs/op
其中106
,表示基准测试的迭代次数,14385391 ns/op
, 表示每次迭代的平均执行时间,52263424 B/op
表示每次迭代的平均内存分配量,42 allocs/op
表示每次迭代的平均分配次数,
上面基准测试的结果,我们主要关注每次迭代需要消耗的内存量,也就是 52263424 B/op
这个数据,这个大概相当于50M左右。在这个示例中,我们使用 ioutil.ReadAll
加载一个10M大小的文件,此时需要分配50M的内存,是文件大小的5倍。
从这里我们可以看出,使用ioutil.ReadAll
加载数据时,存在的一个注意点,便是其分配的内存远远大于待加载数据的大小。
那我们就有疑问了,为什么 ioutil.ReadAll
加载数据时,会消耗这么多内存呢? 下面我们通过说明ioutil.ReadAll
函数的实现,来解释其中的原因。
4. 为什么这么消耗内存
ioutil.ReadAll
函数的实现其实比较简单,ReadAll
函数会初始化一个字节切片缓冲区,然后调用源Reader
的Read
方法不断读取数据,直接读取到EOF
为止。
不过需要注意的是,ReadAll
函数初始化的缓冲区,其初始化大小只有512个字节,在读取过程中,如果缓冲区长度不够,将会不断扩容该缓冲区,直到缓冲区能够容纳所有待读取数据为止。所以调用ioutil.ReadAll
可能会存在多次内存分配的现象。下面我们来看其代码实现:
func ReadAll(r Reader) ([]byte, error) {
// 初始化一个 512 个字节长度的 字节切片
b := make([]byte, 0, 512)
for {
// len(b) == cap(b),此时缓冲区已满,需要扩容
if len(b) == cap(b) {
// 首先append(b,0), 触发切片的扩容机制
// 然后再去掉前面 append 的 '0' 字符
b = append(b, 0)[:len(b)]
}
// 调用Read 方法读取数据
n, err := r.Read(b[len(b):cap(b)])
// 更新切片 len 字段的值
b = b[:len(b)+n]
if err != nil {
// 读取到 EOF, 此时直接返回
if err == EOF {
err = nil
}
return b, err
}
}
}
从上面代码实现来看,使用 ioutil.ReadAll
加载数据需要分配大量内存的原因是因为切片的不断扩容导致的。
ioutil.ReadAll
加载数据时,一开始只初始化了一个512字节大小的切片,如果待加载的数据超过512字节的话,切片会触发扩容操作。同时其也不是一次性扩容到能够容纳所有数据的长度,而是基于切片的扩容机制来决定的。接下来可能会扩容到1024个字节,会重新申请一块内存空间,然后将原切片数据拷贝过去。
之后如果数据超过1024个字节,切片会继续扩容的操作,如此反复,直到切片能够容纳所有的数据为止,这个过程中会存在多次的内存分配的操作,导致大量内存的消耗。
因此,当使用 ioutil.ReadAll
加载数据时,内存消耗会随着数据的大小而增加。特别是在处理大文件或大数据集时,可能需要分配大量的内存空间。这就解释了为什么仅加载一个10M大小的文件,就需要分配50M内存的现象。
5. 替换操作
既然 ioutil.ReadAll
这么消耗内存,那么我们应该尽量避免对其进行使用。但是有时候,我们又需要读取全部数据到内存中,这个时候其实可以使用其他函数来替代ioutil.ReadAll
。下面从文件读取和网络IO读取这两个方面来进行介绍。
5.1 文件读取
ioutil
工具包中,还存在一个ReadFile
的工具函数,能够加载文件的全部数据到内存中,函数定义如下:
func ReadFile(filename string) ([]byte, error) {}
ReadFile
函数的使用非常简单,只需要传入一个待加载文件的路径,返回的数据为文件的内容。下面通过一个基准函数,展示其加载文件时需要的分配内存数等的数据,来和ioutil.ReadAll
做一个比较:
func BenchmarkReadFileMemoryUsage(b *testing.B) {
filePath := "largefile.txt"
for n := 0; n < b.N; n++ {
_, err := ioutil.ReadFile(filePath)
if err != nil {
b.Fatal(err)
}
}
}
上面基准测试运行结果如下:
// ReadFile 函数基准测试结果
BenchmarkReadFileMemoryUsage-4 592 1942212 ns/op 10494290 B/op 5 allocs/op
// ReadAll 函数基准测试结果
BenchmarkReadAllMemoryUsage-4 106 14385391 ns/op 52263424 B/op 42 allocs/op
使用ReadFile
加载整个文件的数据,分配的内存数大概也为10M左右,同时执行时间和内存分配次数,也相对于ReadAll
函数来看,也相对更小。
因此,如果我们确实需要加载文件的全部数据,此时使用ReadFile
相对于ReadAll
肯定是更为合适的。
5.2 网络IO读取
如果是网络IO操作,此时我们需要假定一个前提,是所有的响应数据,应该都是有响应头的,能够通过响应头,获取到响应体的长度,然后再基于此读取全部响应体的数据。
这里可以使用io.Copy
函数来将数据拷贝,从而来替代ioutil.ReadAll
,下面是一个大概代码结构:
package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
// 1. 建立一个网络连接
src := xxx
defer src.Close()
// 2. 读取报文头,获取请求包的长度
size := xxx
// 3. 基于该 size 创建一个 字节切片
buf := make([]byte, size)
buffer := bytes.NewBuffer(buf)
// 4. 使用buffer来读取数据
_, err = io.Copy(&buffer, srcFile)
if err != nil {
fmt.Println("Failed to copy data:", err)
return
}
// 现在数据已加载到内存中的缓冲区(buffer)中
fmt.Println("Data loaded into buffer successfully.")
}
通过这种方式,能够使用io.Copy
函数替换ioutil.ReadAll
,读取到所有的数据,而io.Copy
函数不会存在 ioutil.ReadAll
函数存在的问题。
6. 总结
本文首先对 ioutil.ReadAll
进行了基本的说明,同时给了一个简单的使用示例。
随后,通过基准测试展示了使用 ioutil.ReadAll
加载数据,消耗的内存可能远远大于待加载的数据。之后,通过对源码讲解,说明了导致这个现象导致的原因。
最后,给出了一些替代方案,如使用 ioutil.ReadFile
函数和使用 io.Copy
函数等,以减少内存占用。基于以上内容,便完成了对ioutil.ReadAll
函数的介绍,希望对你有所帮助。
为什么使用ioutil.ReadAll 函数需要注意的更多相关文章
- 为什么要避免在 Go 中使用 ioutil.ReadAll?
原文链接: 为什么要避免在 Go 中使用 ioutil.ReadAll? ioutil.ReadAll 主要的作用是从一个 io.Reader 中读取所有数据,直到结尾. 在 GitHub 上搜索 i ...
- [PHP] 2018年终总结
去掉敏感信息后的不完整版 ==========================================================================2018年12月29日 记 ...
- Golang 笔记 4 defer、error、panic
一.defer语句 defer语句仅能被放置在函数或方法中.它由关键字defer和一个调用表达式组成.这里的表达式所表示的既不能是对Go语言内建函数的调用也不能是对Go语言标准库代码包unsafe中的 ...
- golang学习 ----获取URL
package main import ( "fmt" "io/ioutil" "net/http" "os" ) fu ...
- [日常] go语言圣经-获取URL练习题
1.主要使用net/http和io/ioutil包 2.http.Get函数是创建HTTP请求的函数,resp这个结构体中,Body字段包括一个可读的服务器响应流 3.ioutil.ReadAll函数 ...
- Go 终端读写 && 文件读写、copy
终端读写 操作终端相关文件句柄常量 os.Stdin(standard):标准输入 os.Stdout:标准输出 os.Stderr:标准错误输出 标准输出 demo:直接输出和 判断之后输出的结果不 ...
- ioutil包二
ioutil包二 (原创随笔,转载请注明出处 http://www.cnblogs.com/majianguo/p/8016426.html) ioutil包实现了一些I/O实用功能,导出了7个函数和 ...
- golang bufio、ioutil读文件的速度比较(性能测试)和影响因素分析
前言 golang读取文件的方式主要有4种: 使用File自带的Read方法 使用bufio库的Read方法 使用io/ioutil库的ReadAll() 使用io/ioutil库的ReadFile( ...
- go 学习笔记之解读什么是defer延迟函数
Go 语言中有个 defer 关键字,常用于实现延迟函数来保证关键代码的最终执行,常言道: "未雨绸缪方可有备无患". 延迟函数就是这么一种机制,无论程序是正常返回还是异常报错,只 ...
- 8.Go-Reader,Writer和ioutil
8.1.Reader (1)输入流 流是应用程序和外部资源进行数据交互的纽带 流分为输入流和输出流,输入和输出都是相对于程序,把外部数据传入程序中叫做输入流,反之叫做输出流 在Go语言标准库中io包下 ...
随机推荐
- Hydra详细使用
1. 简介 Hydra是什么 Hydra是什么: Hydra是一款网络登录破解工具,可以通过暴力破解方式来猜解用户名和密码,从而获取系统的访问权限.它可以支持多种协议,如FTP.SSH.Telnet. ...
- 从零开始TP6配置ThinkPHP-ApiDoc
系统:windows11 集成环境:小皮(原phpstudy) composer:2.5 准备工作:安装小皮后,在软件管理中安装composer,2.3安装不上去,只能安装1.8.5,没关系安装后升级 ...
- Linux系统如何查看内核版本信息
使用如下命令: cat /etc/os-release 显示结果如下,系统内核不同,信息不同.
- 使用扩展函数方式,在Winform界面中快捷的绑定树形列表TreeList控件和TreeListLookUpEdit控件
在一些字典绑定中,往往为了方便展示详细数据,需要把一些结构树展现在树列表TreeList控件中或者下拉列表的树形控件TreeListLookUpEdit控件中,为了快速的处理数据的绑定操作,比较每次使 ...
- Godot 4.0 设置应用程序图标、项目图标
godot版本:4.0.2,理论上4.0.0版也适用. 本文章是针对window应用程序而写的,其他平台不一定适用,仅供参考. 效果 输出的可执行文件图标为指定的图标,适配多种尺寸 执行时窗口图标为指 ...
- Costura.Fody 使用问题
1. Costura.Fody 引用后,未能正常合并资源文件.用着用着就不行了 解决方案:在csproj所在的文件目录,找到FodyWeavers.xml,添加<Costura/> 1 & ...
- C# 自定义并动态切换光标
系统有很多光标类型 :Cursors 类 (System.Windows.Input) | Microsoft Docs 本章介绍如何自定义光标.并动态切换光标类型. 动态切换光标类型 以白板书写为例 ...
- P1014 [NOIP1999 普及组] Cantor 表
题目链接:https://www.luogu.com.cn/problem/P1014 有理数可枚举 In 1873 Cantor proved the rational numbers counta ...
- css设置单行多行超出显示省略号
单行: width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; 多行: text-overflow: ...
- 2022-03-29:整个二维平面算是一张地图,给定[x,y],表示你站在x行y列, 你可以选择面朝的任何方向, 给定一个正数值angle,表示你视野的角度为, 这个角度内你可以看无穷远,这个角度外你
2022-03-29:整个二维平面算是一张地图,给定[x,y],表示你站在x行y列, 你可以选择面朝的任何方向, 给定一个正数值angle,表示你视野的角度为, 这个角度内你可以看无穷远,这个角度外你 ...