Go語言使用ioutil.ReadAll函數(shù)需要注意基本說明
1. 引言
當我們需要將數(shù)據(jù)一次性加載到內(nèi)存中,ioutil.ReadAll 函數(shù)是一個方便的選擇,但是ioutil.ReadAll 的使用是需要注意的。
在這篇文章中,我們將首先對ioutil.ReadAll函數(shù)進行基本介紹,之后會介紹其存在的問題,以及引起該問題的原因,最后給出了ioutil.ReadAll 函數(shù)的替代操作。通過這些內(nèi)容,希望能幫助你更好地理解和使用ioutil.ReadAll 函數(shù)。
2. 基本說明
ioutil.ReadAll其實是標準庫的一個函數(shù),其作用是從Reader 參數(shù)讀取所有的數(shù)據(jù),直到遇到EOF為止,函數(shù)定義如下:
func ReadAll(r io.Reader) ([]byte, error)
其中r 為待讀取數(shù)據(jù)的Reader,數(shù)據(jù)讀取結(jié)果將以字節(jié)切片的形式來返回,如果讀取過程中遇到了錯誤,也會返回對應的錯誤。
下面通過一個簡單的示例,來簡單說明ioutil.ReadAll 函數(shù)的使用:
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()
// 讀取文件全部數(shù)據(jù)
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Println("無法讀取文件:%s", err)
return
}
// 將讀取到的數(shù)據(jù)轉(zhuǎn)換為字符串并輸出
content := string(data)
fmt.Println("文件內(nèi)容:")
fmt.Println(content)
}在這個示例中,我們使用os.Open 函數(shù)打開指定路徑的文件,獲取到一個os.File 對象,接著,調(diào)用 ioutil.ReadAll 便能讀取到文件的全部數(shù)據(jù)。
3. 為什么使用 ioutil.ReadAll 需要注意
從上面的基本說明我們可以得知,ioutil.ReadAll 的作用是讀取指定數(shù)據(jù)源的全部數(shù)據(jù),并將其以字節(jié)數(shù)組的形式來返回。比如,我們想要將整個文件的數(shù)據(jù)加載到內(nèi)存中,此時就可以使用 ioutil.ReadAll 函數(shù)來實現(xiàn)。
那這里就有一個問題, 加載一份數(shù)據(jù)到內(nèi)存中,會耗費多少內(nèi)存資源呢? 按照我們的理解,正常是數(shù)據(jù)源數(shù)據(jù)有多大,就大概消耗多大的內(nèi)存資源。
然而,如果使用 ioutil.ReadAll 函數(shù)加載數(shù)據(jù)時消耗的內(nèi)存資源,可能與我們的想法存在一些差距。通常使用 ioutil.ReadAll 函數(shù)加載全部數(shù)據(jù)有可能會消耗更多的內(nèi)存。
下面我們創(chuàng)建一個10M的文件,然后寫一個基準測試函數(shù),來展示使用 ioutil.ReadAll 加載整個文件的數(shù)據(jù),需要分配多少內(nèi)存,函數(shù)如下:
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)
}
}
}基準測試的運行結(jié)果如下:
BenchmarkReadAllMemoryUsage-4 106 14385391 ns/op 52263424 B/op 42 allocs/op
其中106,表示基準測試的迭代次數(shù),14385391 ns/op, 表示每次迭代的平均執(zhí)行時間,52263424 B/op表示每次迭代的平均內(nèi)存分配量,42 allocs/op 表示每次迭代的平均分配次數(shù),
上面基準測試的結(jié)果,我們主要關(guān)注每次迭代需要消耗的內(nèi)存量,也就是 52263424 B/op 這個數(shù)據(jù),這個大概相當于50M左右。在這個示例中,我們使用 ioutil.ReadAll 加載一個10M大小的文件,此時需要分配50M的內(nèi)存,是文件大小的5倍。
從這里我們可以看出,使用ioutil.ReadAll 加載數(shù)據(jù)時,存在的一個注意點,便是其分配的內(nèi)存遠遠大于待加載數(shù)據(jù)的大小。
那我們就有疑問了,為什么 ioutil.ReadAll 加載數(shù)據(jù)時,會消耗這么多內(nèi)存呢? 下面我們通過說明ioutil.ReadAll 函數(shù)的實現(xiàn),來解釋其中的原因。
4. 為什么這么消耗內(nèi)存
ioutil.ReadAll 函數(shù)的實現(xiàn)其實比較簡單,ReadAll 函數(shù)會初始化一個字節(jié)切片緩沖區(qū),然后調(diào)用源Reader 的Read 方法不斷讀取數(shù)據(jù),直接讀取到EOF 為止。
不過需要注意的是,ReadAll 函數(shù)初始化的緩沖區(qū),其初始化大小只有512個字節(jié),在讀取過程中,如果緩沖區(qū)長度不夠,將會不斷擴容該緩沖區(qū),直到緩沖區(qū)能夠容納所有待讀取數(shù)據(jù)為止。所以調(diào)用ioutil.ReadAll 可能會存在多次內(nèi)存分配的現(xiàn)象。下面我們來看其代碼實現(xiàn):
func ReadAll(r Reader) ([]byte, error) {
// 初始化一個 512 個字節(jié)長度的 字節(jié)切片
b := make([]byte, 0, 512)
for {
// len(b) == cap(b),此時緩沖區(qū)已滿,需要擴容
if len(b) == cap(b) {
// 首先append(b,0), 觸發(fā)切片的擴容機制
// 然后再去掉前面 append 的 '0' 字符
b = append(b, 0)[:len(b)]
}
// 調(diào)用Read 方法讀取數(shù)據(jù)
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
}
}
}從上面代碼實現(xiàn)來看,使用 ioutil.ReadAll 加載數(shù)據(jù)需要分配大量內(nèi)存的原因是因為切片的不斷擴容導致的。
ioutil.ReadAll 加載數(shù)據(jù)時,一開始只初始化了一個512字節(jié)大小的切片,如果待加載的數(shù)據(jù)超過512字節(jié)的話,切片會觸發(fā)擴容操作。同時其也不是一次性擴容到能夠容納所有數(shù)據(jù)的長度,而是基于切片的擴容機制來決定的。接下來可能會擴容到1024個字節(jié),會重新申請一塊內(nèi)存空間,然后將原切片數(shù)據(jù)拷貝過去。
之后如果數(shù)據(jù)超過1024個字節(jié),切片會繼續(xù)擴容的操作,如此反復,直到切片能夠容納所有的數(shù)據(jù)為止,這個過程中會存在多次的內(nèi)存分配的操作,導致大量內(nèi)存的消耗。
因此,當使用 ioutil.ReadAll加載數(shù)據(jù)時,內(nèi)存消耗會隨著數(shù)據(jù)的大小而增加。特別是在處理大文件或大數(shù)據(jù)集時,可能需要分配大量的內(nèi)存空間。這就解釋了為什么僅加載一個10M大小的文件,就需要分配50M內(nèi)存的現(xiàn)象。
5. 替換操作
既然 ioutil.ReadAll 這么消耗內(nèi)存,那么我們應該盡量避免對其進行使用。但是有時候,我們又需要讀取全部數(shù)據(jù)到內(nèi)存中,這個時候其實可以使用其他函數(shù)來替代ioutil.ReadAll。下面從文件讀取和網(wǎng)絡(luò)IO讀取這兩個方面來進行介紹。
5.1 文件讀取
ioutil 工具包中,還存在一個ReadFile的工具函數(shù),能夠加載文件的全部數(shù)據(jù)到內(nèi)存中,函數(shù)定義如下:
func ReadFile(filename string) ([]byte, error) {}ReadFile函數(shù)的使用非常簡單,只需要傳入一個待加載文件的路徑,返回的數(shù)據(jù)為文件的內(nèi)容。下面通過一個基準函數(shù),展示其加載文件時需要的分配內(nèi)存數(shù)等的數(shù)據(jù),來和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)
}
}
}上面基準測試運行結(jié)果如下:
// ReadFile 函數(shù)基準測試結(jié)果 BenchmarkReadFileMemoryUsage-4 592 1942212 ns/op 10494290 B/op 5 allocs/op // ReadAll 函數(shù)基準測試結(jié)果 BenchmarkReadAllMemoryUsage-4 106 14385391 ns/op 52263424 B/op 42 allocs/op
使用ReadFile加載整個文件的數(shù)據(jù),分配的內(nèi)存數(shù)大概也為10M左右,同時執(zhí)行時間和內(nèi)存分配次數(shù),也相對于ReadAll 函數(shù)來看,也相對更小。
因此,如果我們確實需要加載文件的全部數(shù)據(jù),此時使用ReadFile相對于ReadAll 肯定是更為合適的。
5.2 網(wǎng)絡(luò)IO讀取
如果是網(wǎng)絡(luò)IO操作,此時我們需要假定一個前提,是所有的響應數(shù)據(jù),應該都是有響應頭的,能夠通過響應頭,獲取到響應體的長度,然后再基于此讀取全部響應體的數(shù)據(jù)。
這里可以使用io.Copy函數(shù)來將數(shù)據(jù)拷貝,從而來替代ioutil.ReadAll,下面是一個大概代碼結(jié)構(gòu):
package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
// 1. 建立一個網(wǎng)絡(luò)連接
src := xxx
defer src.Close()
// 2. 讀取報文頭,獲取請求包的長度
size := xxx
// 3. 基于該 size 創(chuàng)建一個 字節(jié)切片
buf := make([]byte, size)
buffer := bytes.NewBuffer(buf)
// 4. 使用buffer來讀取數(shù)據(jù)
_, err = io.Copy(&buffer, srcFile)
if err != nil {
fmt.Println("Failed to copy data:", err)
return
}
// 現(xiàn)在數(shù)據(jù)已加載到內(nèi)存中的緩沖區(qū)(buffer)中
fmt.Println("Data loaded into buffer successfully.")
}通過這種方式,能夠使用io.Copy 函數(shù)替換ioutil.ReadAll ,讀取到所有的數(shù)據(jù),而io.Copy 函數(shù)不會存在 ioutil.ReadAll 函數(shù)存在的問題。
6. 總結(jié)
本文首先對 ioutil.ReadAll 進行了基本的說明,同時給了一個簡單的使用示例。
隨后,通過基準測試展示了使用 ioutil.ReadAll 加載數(shù)據(jù),消耗的內(nèi)存可能遠遠大于待加載的數(shù)據(jù)。之后,通過對源碼講解,說明了導致這個現(xiàn)象導致的原因。
最后,給出了一些替代方案,如使用 ioutil.ReadFile 函數(shù)和使用 io.Copy 函數(shù)等,以減少內(nèi)存占用?;谝陨蟽?nèi)容,便完成了對ioutil.ReadAll 函數(shù)的介紹,希望對你有所幫助。
更多關(guān)于go ioutil.ReadAll函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡單聊聊Go語言中空結(jié)構(gòu)體和空字符串的特殊之處
在日常的編程過程中,大家應該經(jīng)常能遇到各種”空“吧,比如空指針、空結(jié)構(gòu)體、空字符串等,本文就以?Go?語言為例,一起來看看空結(jié)構(gòu)體和空字符串在?Go?語言中的特殊之處吧2024-03-03

