Go语言使用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 函数的介绍,希望对你有所帮助。

更多关于go ioutil.ReadAll函数的资料请关注aitechtogether.com其它相关文章!

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年12月27日
下一篇 2023年12月27日

相关推荐