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