自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么要避免在 Go 中使用 ioutil.ReadAll?

開發(fā) 前端
本篇文章從一個問題入手,重點研究了 ioutil.ReadAll 函數(shù)。主要原因是在小數(shù)據(jù)量的情況下,這個函數(shù)并沒有什么問題,但當數(shù)據(jù)量大時,它就變成了一顆定時炸彈。有可能會影響程序的性能,甚至會導致程序崩潰。

ioutil.ReadAll 主要的作用是從一個 io.Reader 中讀取所有數(shù)據(jù),直到結尾。

在 GitHub 上搜索 ioutil.ReadAll,類型選擇 Code,語言選擇 Go,一共得到了 637307 條結果。

這說明 ioutil.ReadAll 還是挺受歡迎的,主要也是用起來確實方便。

但是當遇到大文件時,這個函數(shù)就會暴露出兩個明顯的缺點:

性能問題,文件越大,性能越差。

文件過大的話,可能直接撐爆內存,導致程序崩潰。

為什么會這樣呢?這篇文章就通過源碼來分析背后的原因,并試圖給出更好的解決方案。

下面我們正式開始。

ioutil.ReadAll

首先,我們通過一個例子看一下 ioutil.ReadAll 的使用場景。比如說,使用 http.Client 發(fā)送 GET 請求,然后再讀取返回內容:

  1. func main() { 
  2.     res, err := http.Get("http://www.google.com/robots.txt"
  3.     if err != nil { 
  4.         log.Fatal(err) 
  5.     } 
  6.  
  7.     robots, err := io.ReadAll(res.Body) 
  8.     res.Body.Close() 
  9.     if err != nil { 
  10.         log.Fatal(err) 
  11.     } 
  12.     fmt.Printf("%s", robots) 

http.Get() 返回的數(shù)據(jù),存儲在 res.Body 中,通過 ioutil.ReadAll 將其讀取出來。

表面上看這段代碼沒有什么問題,但仔細分析卻并非如此。想要探究其背后的原因,就只能靠源碼說話。

ioutil.ReadAll 的源碼如下:

  1. // src/io/ioutil/ioutil.go 
  2.  
  3. func ReadAll(r io.Reader) ([]byte, error) { 
  4.     return io.ReadAll(r) 

Go 1.16 版本開始,直接調用 io.ReadAll() 函數(shù),下面再看看 io.ReadAll() 的實現(xiàn):

  1. // src/io/io.go 
  2.  
  3. func ReadAll(r Reader) ([]byte, error) { 
  4.     // 創(chuàng)建一個 512 字節(jié)的 buf 
  5.     b := make([]byte, 0, 512) 
  6.     for { 
  7.         if len(b) == cap(b) { 
  8.             // 如果 buf 滿了,則追加一個元素,使其重新分配內存 
  9.             b = append(b, 0)[:len(b)] 
  10.         } 
  11.         // 讀取內容到 buf 
  12.         n, err := r.Read(b[len(b):cap(b)]) 
  13.         b = b[:len(b)+n] 
  14.         // 遇到結尾或者報錯則返回 
  15.         if err != nil { 
  16.             if err == EOF { 
  17.                 err = nil 
  18.             } 
  19.             return b, err 
  20.         } 
  21.     } 

我給代碼加上了必要的注釋,這段代碼的執(zhí)行主要分三個步驟:

  • 創(chuàng)建一個 512 字節(jié)的 buf;
  • 不斷讀取內容到 buf,當 buf 滿的時候,會追加一個元素,促使其重新分配內存;
  • 直到結尾或報錯,則返回;

知道了執(zhí)行步驟,但想要分析其性能問題,還需要了解 Go 切片的擴容策略,如下:

  • 如果期望容量大于當前容量的兩倍就會使用期望容量;
  • 如果當前切片的長度小于 1024 就會將容量翻倍;
  • 如果當前切片的長度大于 1024 就會每次增加 25% 的容量,直到新容量大于期望容量;

也就是說,如果待拷貝數(shù)據(jù)的容量小于 512 字節(jié)的話,性能不受影響。但如果超過 512 字節(jié),就會開始切片擴容。數(shù)據(jù)量越大,擴容越頻繁,性能受影響越大。

如果數(shù)據(jù)量足夠大的話,內存可能就直接撐爆了,這樣的話影響就大了。

那有更好的替換方案嗎?當然是有的,我們接著往下看。

io.Copy

可以使用 io.Copy 函數(shù)來代替,源碼定義如下:

  1. src/io/io.go 
  2.  
  3. func Copy(dst Writer, src Reader) (written int64, err error) { 
  4.     return copyBuffer(dst, src, nil) 

其功能是直接從 src 讀取數(shù)據(jù),并寫入到 dst。

和 ioutil.ReadAll 最大的不同就是沒有把所有數(shù)據(jù)一次性都取出來,而是不斷讀取,不斷寫入。

具體實現(xiàn) Copy 的邏輯在 copyBuffer 函數(shù)中實現(xiàn):

  1. // src/io/io.go 
  2.  
  3. func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) { 
  4.     // 如果源實現(xiàn)了 WriteTo 方法,則直接調用 WriteTo 
  5.     if wt, ok := src.(WriterTo); ok { 
  6.         return wt.WriteTo(dst) 
  7.     } 
  8.     // 同樣的,如果目標實現(xiàn)了 ReaderFrom 方法,則直接調用 ReaderFrom 
  9.     if rt, ok := dst.(ReaderFrom); ok { 
  10.         return rt.ReadFrom(src) 
  11.     } 
  12.     // 如果 buf 為空,則創(chuàng)建 32KB 的 buf 
  13.     if buf == nil { 
  14.         size := 32 * 1024 
  15.         if l, ok := src.(*LimitedReader); ok && int64(size) > l.N { 
  16.             if l.N < 1 { 
  17.                 size = 1 
  18.             } else { 
  19.                 size = int(l.N) 
  20.             } 
  21.         } 
  22.         buf = make([]byte, size
  23.     } 
  24.     // 循環(huán)讀取數(shù)據(jù)并寫入 
  25.     for { 
  26.         nr, er := src.Read(buf) 
  27.         if nr > 0 { 
  28.             nw, ew := dst.Write(buf[0:nr]) 
  29.             if nw < 0 || nr < nw { 
  30.                 nw = 0 
  31.                 if ew == nil { 
  32.                     ew = errInvalidWrite 
  33.                 } 
  34.             } 
  35.             written += int64(nw) 
  36.             if ew != nil { 
  37.                 err = ew 
  38.                 break 
  39.             } 
  40.             if nr != nw { 
  41.                 err = ErrShortWrite 
  42.                 break 
  43.             } 
  44.         } 
  45.         if er != nil { 
  46.             if er != EOF { 
  47.                 err = er 
  48.             } 
  49.             break 
  50.         } 
  51.     } 
  52.     return written, err 

此函數(shù)執(zhí)行步驟如下:

如果源實現(xiàn)了 WriteTo 方法,則直接調用 WriteTo 方法;

同樣的,如果目標實現(xiàn)了 ReaderFrom 方法,則直接調用 ReaderFrom 方法;

如果 buf 為空,則創(chuàng)建 32KB 的 buf;

最后就是循環(huán) Read 和 Write;

對比之后就會發(fā)現(xiàn),io.Copy 函數(shù)不會一次性讀取全部數(shù)據(jù),也不會頻繁進行切片擴容,顯然在數(shù)據(jù)量大時是更好的選擇。

ioutil 其他函數(shù)

再看看 ioutil 包的其他函數(shù):

  • func ReadDir(dirname string) ([]os.FileInfo, error)
  • func ReadFile(filename string) ([]byte, error)
  • func WriteFile(filename string, data []byte, perm os.FileMode) error
  • func TempFile(dir, prefix string) (f *os.File, err error)
  • func TempDir(dir, prefix string) (name string, err error)
  • func NopCloser(r io.Reader) io.ReadCloser

下面舉例詳細說明:

ReadDir

  1. // ReadDir 讀取指定目錄中的所有目錄和文件(不包括子目錄)。 
  2. // 返回讀取到的文件信息列表和遇到的錯誤,列表是經(jīng)過排序的。 
  3. func ReadDir(dirname string) ([]os.FileInfo, error) 

舉例:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.  
  7. func main() { 
  8.     dirName := "../" 
  9.     fileInfos, _ := ioutil.ReadDir(dirName) 
  10.     fmt.Println(len(fileInfos)) 
  11.     for i := 0; i < len(fileInfos); i++ { 
  12.         fmt.Printf("%T\n", fileInfos[i]) 
  13.         fmt.Println(i, fileInfos[i].Name(), fileInfos[i].IsDir()) 
  14.  
  15.     } 

ReadFile

  1. // ReadFile 讀取文件中的所有數(shù)據(jù),返回讀取的數(shù)據(jù)和遇到的錯誤 
  2. // 如果讀取成功,則 err 返回 nil,而不是 EOF 
  3. func ReadFile(filename string) ([]byte, error) 

舉例:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.     "os" 
  7.  
  8. func main() { 
  9.     data, err := ioutil.ReadFile("./test.txt"
  10.     if err != nil { 
  11.         fmt.Println("read error"
  12.         os.Exit(1) 
  13.     } 
  14.     fmt.Println(string(data)) 

WriteFile

  1. // WriteFile 向文件中寫入數(shù)據(jù),寫入前會清空文件。 
  2. // 如果文件不存在,則會以指定的權限創(chuàng)建該文件。 
  3. // 返回遇到的錯誤。 
  4. func WriteFile(filename string, data []byte, perm os.FileMode) error 

舉例:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.  
  7. func main() { 
  8.     fileName := "./text.txt" 
  9.     s := "Hello AlwaysBeta" 
  10.     err := ioutil.WriteFile(fileName, []byte(s), 0777) 
  11.     fmt.Println(err) 

TempFile

  1. // TempFile 在 dir 目錄中創(chuàng)建一個以 prefix 為前綴的臨時文件,并將其以讀 
  2. // 寫模式打開。返回創(chuàng)建的文件對象和遇到的錯誤。 
  3. // 如果 dir 為空,則在默認的臨時目錄中創(chuàng)建文件(參見 os.TempDir),多次 
  4. // 調用會創(chuàng)建不同的臨時文件,調用者可以通過 f.Name() 獲取文件的完整路徑。 
  5. // 調用本函數(shù)所創(chuàng)建的臨時文件,應該由調用者自己刪除。 
  6. func TempFile(dir, prefix string) (f *os.File, err error) 

舉例:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.     "os" 
  7.  
  8. func main() { 
  9.     f, err := ioutil.TempFile("./""Test"
  10.     if err != nil { 
  11.         fmt.Println(err) 
  12.     } 
  13.     defer os.Remove(f.Name()) // 用完刪除 
  14.     fmt.Printf("%s\n", f.Name()) 

TempDir

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.     "os" 
  7.  
  8. func main() { 
  9.     dir, err := ioutil.TempDir("./""Test"
  10.     if err != nil { 
  11.         fmt.Println(err) 
  12.     } 
  13.     defer os.Remove(dir) // 用完刪除 
  14.     fmt.Printf("%s\n", dir) 

NopCloser

  1. // NopCloser 將 r 包裝為一個 ReadCloser 類型,但 Close 方法不做任何事情。 
  2. func NopCloser(r io.Reader) io.ReadCloser 

這個函數(shù)的使用場景是這樣的:

有時候我們需要傳遞一個 io.ReadCloser 的實例,而我們現(xiàn)在有一個 io.Reader 的實例,比如:strings.Reader。

這個時候 NopCloser 就派上用場了。它包裝一個 io.Reader,返回一個 io.ReadCloser,相應的 Close 方法啥也不做,只是返回 nil。

舉例:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "io/ioutil" 
  6.     "reflect" 
  7.     "strings" 
  8.  
  9. func main() { 
  10.     //返回 *strings.Reader 
  11.     reader := strings.NewReader("Hello AlwaysBeta"
  12.     r := ioutil.NopCloser(reader) 
  13.     defer r.Close() 
  14.  
  15.     fmt.Println(reflect.TypeOf(reader)) 
  16.     data, _ := ioutil.ReadAll(reader) 
  17.     fmt.Println(string(data)) 

總結

ioutil 提供了幾個很實用的工具函數(shù),背后實現(xiàn)邏輯也并不復雜。

本篇文章從一個問題入手,重點研究了 ioutil.ReadAll 函數(shù)。主要原因是在小數(shù)據(jù)量的情況下,這個函數(shù)并沒有什么問題,但當數(shù)據(jù)量大時,它就變成了一顆定時炸彈。有可能會影響程序的性能,甚至會導致程序崩潰。

接下來給出對應的解決方案,在數(shù)據(jù)量大的情況下,最好使用 io.Copy 函數(shù)。

文章最后繼續(xù)介紹了 ioutil 的其他幾個函數(shù),并給出了程序示例。相關代碼都會上傳到 GitHub,需要的同學可以自行下載。

好了,本文就到這里吧。關注我,帶你通過問題讀 Go 源碼。

源碼地址:

https://github.com/yongxinz/gopher

 

責任編輯:武曉燕 來源: AlwaysBeta
相關推薦

2022-12-26 00:25:06

2021-04-26 09:33:46

Go Iota語言

2022-06-02 08:48:39

Go枚舉器Iota

2025-01-15 09:06:58

CSSRegEx前端

2023-11-27 19:39:46

Goprotobuf

2022-01-17 16:09:43

Go語言開發(fā)

2024-07-08 00:01:00

GPM模型調度器

2021-10-13 14:06:46

MySQLUtf8符號

2021-11-29 22:59:34

Go Dockertest集成

2023-10-28 16:22:21

Go接口

2023-01-11 08:06:42

對象賦值項目開發(fā)

2022-06-30 08:03:13

Prisma數(shù)據(jù)庫工具開源

2020-05-25 15:37:47

物聯(lián)網(wǎng)設備密碼物聯(lián)網(wǎng)安全

2020-02-24 09:38:05

PythonGo語言Linux

2020-06-22 07:18:21

Java語言開發(fā)

2024-01-02 10:38:22

Go語言數(shù)組

2013-07-29 14:50:43

API

2023-11-30 09:00:00

TypeScript開發(fā)

2016-03-24 09:53:24

swiftguardios

2021-06-17 06:19:20

存儲SQL數(shù)據(jù)庫
點贊
收藏

51CTO技術棧公眾號