成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

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

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

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

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

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

下面我們正式開始。

ioutil.ReadAll

首先,我們通過一個例子看一下 ioutil.ReadAll 的使用場景。比如說,使用 http.Client 發送 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() 返回的數據,存儲在 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() 函數,下面再看看 io.ReadAll() 的實現:

  1. // src/io/io.go 
  2.  
  3. func ReadAll(r Reader) ([]byte, error) { 
  4.     // 創建一個 512 字節的 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.     } 

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

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

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

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

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

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

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

io.Copy

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

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

其功能是直接從 src 讀取數據,并寫入到 dst。

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

具體實現 Copy 的邏輯在 copyBuffer 函數中實現:

  1. // src/io/io.go 
  2.  
  3. func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) { 
  4.     // 如果源實現了 WriteTo 方法,則直接調用 WriteTo 
  5.     if wt, ok := src.(WriterTo); ok { 
  6.         return wt.WriteTo(dst) 
  7.     } 
  8.     // 同樣的,如果目標實現了 ReaderFrom 方法,則直接調用 ReaderFrom 
  9.     if rt, ok := dst.(ReaderFrom); ok { 
  10.         return rt.ReadFrom(src) 
  11.     } 
  12.     // 如果 buf 為空,則創建 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.     // 循環讀取數據并寫入 
  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 

此函數執行步驟如下:

如果源實現了 WriteTo 方法,則直接調用 WriteTo 方法;

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

如果 buf 為空,則創建 32KB 的 buf;

最后就是循環 Read 和 Write;

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

ioutil 其他函數

再看看 ioutil 包的其他函數:

  • 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. // 返回讀取到的文件信息列表和遇到的錯誤,列表是經過排序的。 
  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 讀取文件中的所有數據,返回讀取的數據和遇到的錯誤 
  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 向文件中寫入數據,寫入前會清空文件。 
  2. // 如果文件不存在,則會以指定的權限創建該文件。 
  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 目錄中創建一個以 prefix 為前綴的臨時文件,并將其以讀 
  2. // 寫模式打開。返回創建的文件對象和遇到的錯誤。 
  3. // 如果 dir 為空,則在默認的臨時目錄中創建文件(參見 os.TempDir),多次 
  4. // 調用會創建不同的臨時文件,調用者可以通過 f.Name() 獲取文件的完整路徑。 
  5. // 調用本函數所創建的臨時文件,應該由調用者自己刪除。 
  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 

這個函數的使用場景是這樣的:

有時候我們需要傳遞一個 io.ReadCloser 的實例,而我們現在有一個 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 提供了幾個很實用的工具函數,背后實現邏輯也并不復雜。

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

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

文章最后繼續介紹了 ioutil 的其他幾個函數,并給出了程序示例。相關代碼都會上傳到 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語言開發

2024-07-08 00:01:00

GPM模型調度器

2021-10-13 14:06:46

MySQLUtf8符號

2023-10-28 16:22:21

Go接口

2021-11-29 22:59:34

Go Dockertest集成

2023-01-11 08:06:42

對象賦值項目開發

2022-06-30 08:03:13

Prisma數據庫工具開源

2020-05-25 15:37:47

物聯網設備密碼物聯網安全

2013-07-29 14:50:43

API

2024-01-02 10:38:22

Go語言數組

2020-06-22 07:18:21

Java語言開發

2020-02-24 09:38:05

PythonGo語言Linux

2016-03-24 09:53:24

swiftguardios

2023-11-30 09:00:00

TypeScript開發

2019-01-27 14:37:47

數據HTTP服務
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美精品一区在线发布 | 国产精品视频观看 | 欧美一区二区久久 | 午夜大片 | 欧美成人hd| 成人精品国产免费网站 | 九色视频网站 | 久久久久一区 | 一区二区三区在线观看免费视频 | 中文字幕一页二页 | 日本黄色短片 | 中文字幕在线观看一区二区 | 国产一区二区在线免费 | 亚洲欧美精品在线 | 日韩成人影院 | 国产精品久久久久久久久图文区 | 99久久婷婷国产综合精品电影 | 本道综合精品 | 北条麻妃一区二区三区在线观看 | 看av片网站 | 91精品导航 | a毛片| 国产精品污www一区二区三区 | 午夜小视频在线播放 | 91.com在线观看 | 日韩三级精品 | 美日韩免费视频 | 日本午夜免费福利视频 | 欧美一区二区二区 | 日韩欧美一区二区三区免费观看 | 亚洲精品亚洲人成人网 | 久久一二区 | 粉嫩粉嫩芽的虎白女18在线视频 | 久久精品亚洲精品国产欧美 | 狠狠色狠狠色综合系列 | 亚洲精品一二三区 | 国产精品免费在线 | 亚洲三级国产 | 国产精品99 | 久久99网 | 成人在线视频免费观看 |