为什么使用ioutil.ReadAll 函数需要注意

这篇具有很好参考价值的文章主要介绍了为什么使用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 函数会初始化一个字节切片缓冲区,然后调用源ReaderRead 方法不断读取数据,直接读取到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 函数的介绍,希望对你有所帮助。文章来源地址https://www.toymoban.com/news/detail-543995.html

到了这里,关于为什么使用ioutil.ReadAll 函数需要注意的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C&C++】为什么 scanf 函数在读取字符串时不需要用取地址运算符 &

    在C语言中,字符串实际上是字符数组,所以我们可以使用 scanf 函数来读取字符串。但是,需要注意的是, scanf 在读取字符串时会在遇到空格、制表符或换行符时停止。因此,它不能用于读取包含空格的字符串。 以下是使用 scanf 读取字符串的基本示例: 在这个例子中,我们

    2024年01月20日
    浏览(37)
  • 为什么需要在bean上使用@EqualsAndHashCode(callSuper = true)这个注解

    我们还是写一个案例,来探讨它的作用。 写三个类,一个是子类Child.class,一个是父类Person.class,一个是测试类Test.class Person父类很简单,就一个属性code; Child子类也很简单,继承了父类之后,自己也是定义了一个属性name; Test类: 创建了两个Child,属性code是不一样的,一个

    2024年02月03日
    浏览(29)
  • 为什么kafka 需要 subscribe 的 group.id?我们是否需要使用 commitSync 手动提交偏移量?

    消费概念: Kafka 使用消费者组的概念来实现主题的并行消费 - 每条消息都将在每个消费者组中传递一次,无论该组中实际有多少个消费者。所以 group 参数是强制性的,如果没有组,Kafka 将不知道如何对待订阅同一主题的其他消费者。 偏移量 : 每当我们启动一个消费者时,

    2024年02月12日
    浏览(44)
  • C——Union是什么?Union和Struct这么像,区别在哪?为什么还要创造出union呢?需要在哪里使用呢?

    在 C 语言中,有一种叫做 union 的变量,是用来在不同的情况下,存放不同类型和大小的对象的变量。这与结构体 struct 很相似:结构体是一个或多个变量的集合。 union 的声明方式非常简单,也与 struct 一模一样,如下: 可以看到,唯一的不同就是结构体中的 struct 变成了 un

    2024年02月03日
    浏览(43)
  • 为什么js中不推荐使用eval函数

    \\\'eval\\\'函数是javascript中的一个内置函数,它的主要作用是将传入的字符串作为代码来执行。换句话说,\\\'eval\\\'可以将动态生成的字符串当作javascript代码来执行,并返回执行结果。 我的理解就是它可以执行传入的代码,并返回执行结果。 \\\'eval\\\'可以执行任何传入的字符串,所以意味

    2024年02月08日
    浏览(40)
  • 网络编程——RPC与HTTP基本介绍、历史追溯、主流应用场景、对比分析、为什么还需要使用RPC

    HTTP协议(Hyper Text Transfer Protocol) 超文本传输协议 : 一个用于在网络上交换信息的标准协议,它定义了客户端(例如浏览器)和服务器之间的通信方式。如平时上网在浏览器上敲个网址url就能访问网页,这里用到的就是HTTP协议。 明确 HTTP 是一个协议,是一个超文本传输协议,

    2024年02月16日
    浏览(31)
  • Vue-33、Vue中为什么使用render函数

    1、main.js 2、查看引入vue 引入的dist/vue.runtime.esm.js 查看vue.runtime.esm.js 大小 此时引入的是残缺的Vue 无模板解析器。打包之后体积小一点。 而render: h = h(App), 可以进行模板解析。

    2024年01月23日
    浏览(30)
  • 为什么需要websocket?

    前端和后端的交互模式最常见的就是前端发数据请求,从后端拿到数据后展示到页面中。如果前端不做操作,后端不能主动向前端推送数据,这也是http协议的缺陷。        因此,一种新的通信协议应运而生---websocket,他最大的特点就是服务端可以主动向客户端推送消息,客

    2024年02月12日
    浏览(44)
  • 为什么需要单元测试?

    为什么需要单元测试? 从产品角度而言,常规的功能测试、系统测试都是站在产品局部或全局功能进行测试,能够很好地与用户的需要相结合,但是缺乏了对产品研发细节(特别是代码细节的理解)。 从测试人员角度而言,功能测试和系统测试以及其他性能测试等等对测试

    2024年02月12日
    浏览(54)
  • 为什么需要uboot?

    bootROM: 一种固化在芯片内部的只读存储器(ROM),用于启动和初始化系统。BootROM 中通常包含了一些预先编写好的代码,用于完成系统启动前的基本初始化和配置, 例如初始化时钟、GPIO控制器、中断控制器、存储设备(SD卡、NAND Flash、SPicy Flash)等硬件资源, 检测启动设备

    2023年04月23日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包