為什么要避免在 Go 中使用 ioutil.ReadAll?
ioutil.ReadAll 主要的作用是從一個 io.Reader 中讀取所有數(shù)據(jù),直到結尾。
在 GitHub 上搜索 ioutil.ReadAll,類型選擇 Code,語言選擇 Go,一共得到了 637307 條結果。
這說明 ioutil.ReadAll 還是挺受歡迎的,主要也是用起來確實方便。
但是當遇到大文件時,這個函數(shù)就會暴露出兩個明顯的缺點:
性能問題,文件越大,性能越差。
文件過大的話,可能直接撐爆內存,導致程序崩潰。
為什么會這樣呢?這篇文章就通過源碼來分析背后的原因,并試圖給出更好的解決方案。
下面我們正式開始。
ioutil.ReadAll
首先,我們通過一個例子看一下 ioutil.ReadAll 的使用場景。比如說,使用 http.Client 發(fā)送 GET 請求,然后再讀取返回內容:
- func main() {
- res, err := http.Get("http://www.google.com/robots.txt")
- if err != nil {
- log.Fatal(err)
- }
- robots, err := io.ReadAll(res.Body)
- res.Body.Close()
- if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("%s", robots)
- }
http.Get() 返回的數(shù)據(jù),存儲在 res.Body 中,通過 ioutil.ReadAll 將其讀取出來。
表面上看這段代碼沒有什么問題,但仔細分析卻并非如此。想要探究其背后的原因,就只能靠源碼說話。
ioutil.ReadAll 的源碼如下:
- // src/io/ioutil/ioutil.go
- func ReadAll(r io.Reader) ([]byte, error) {
- return io.ReadAll(r)
- }
Go 1.16 版本開始,直接調用 io.ReadAll() 函數(shù),下面再看看 io.ReadAll() 的實現(xiàn):
- // src/io/io.go
- func ReadAll(r Reader) ([]byte, error) {
- // 創(chuàng)建一個 512 字節(jié)的 buf
- b := make([]byte, 0, 512)
- for {
- if len(b) == cap(b) {
- // 如果 buf 滿了,則追加一個元素,使其重新分配內存
- b = append(b, 0)[:len(b)]
- }
- // 讀取內容到 buf
- n, err := r.Read(b[len(b):cap(b)])
- b = b[:len(b)+n]
- // 遇到結尾或者報錯則返回
- if err != nil {
- if err == EOF {
- err = nil
- }
- return b, err
- }
- }
- }
我給代碼加上了必要的注釋,這段代碼的執(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ù)來代替,源碼定義如下:
- src/io/io.go
- func Copy(dst Writer, src Reader) (written int64, err error) {
- return copyBuffer(dst, src, nil)
- }
其功能是直接從 src 讀取數(shù)據(jù),并寫入到 dst。
和 ioutil.ReadAll 最大的不同就是沒有把所有數(shù)據(jù)一次性都取出來,而是不斷讀取,不斷寫入。
具體實現(xiàn) Copy 的邏輯在 copyBuffer 函數(shù)中實現(xiàn):
- // src/io/io.go
- func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
- // 如果源實現(xiàn)了 WriteTo 方法,則直接調用 WriteTo
- if wt, ok := src.(WriterTo); ok {
- return wt.WriteTo(dst)
- }
- // 同樣的,如果目標實現(xiàn)了 ReaderFrom 方法,則直接調用 ReaderFrom
- if rt, ok := dst.(ReaderFrom); ok {
- return rt.ReadFrom(src)
- }
- // 如果 buf 為空,則創(chuàng)建 32KB 的 buf
- if buf == nil {
- size := 32 * 1024
- if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
- if l.N < 1 {
- size = 1
- } else {
- size = int(l.N)
- }
- }
- buf = make([]byte, size)
- }
- // 循環(huán)讀取數(shù)據(jù)并寫入
- for {
- nr, er := src.Read(buf)
- if nr > 0 {
- nw, ew := dst.Write(buf[0:nr])
- if nw < 0 || nr < nw {
- nw = 0
- if ew == nil {
- ew = errInvalidWrite
- }
- }
- written += int64(nw)
- if ew != nil {
- err = ew
- break
- }
- if nr != nw {
- err = ErrShortWrite
- break
- }
- }
- if er != nil {
- if er != EOF {
- err = er
- }
- break
- }
- }
- 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
- // ReadDir 讀取指定目錄中的所有目錄和文件(不包括子目錄)。
- // 返回讀取到的文件信息列表和遇到的錯誤,列表是經(jīng)過排序的。
- func ReadDir(dirname string) ([]os.FileInfo, error)
舉例:
- package main
- import (
- "fmt"
- "io/ioutil"
- )
- func main() {
- dirName := "../"
- fileInfos, _ := ioutil.ReadDir(dirName)
- fmt.Println(len(fileInfos))
- for i := 0; i < len(fileInfos); i++ {
- fmt.Printf("%T\n", fileInfos[i])
- fmt.Println(i, fileInfos[i].Name(), fileInfos[i].IsDir())
- }
- }
ReadFile
- // ReadFile 讀取文件中的所有數(shù)據(jù),返回讀取的數(shù)據(jù)和遇到的錯誤
- // 如果讀取成功,則 err 返回 nil,而不是 EOF
- func ReadFile(filename string) ([]byte, error)
舉例:
- package main
- import (
- "fmt"
- "io/ioutil"
- "os"
- )
- func main() {
- data, err := ioutil.ReadFile("./test.txt")
- if err != nil {
- fmt.Println("read error")
- os.Exit(1)
- }
- fmt.Println(string(data))
- }
WriteFile
- // WriteFile 向文件中寫入數(shù)據(jù),寫入前會清空文件。
- // 如果文件不存在,則會以指定的權限創(chuàng)建該文件。
- // 返回遇到的錯誤。
- func WriteFile(filename string, data []byte, perm os.FileMode) error
舉例:
- package main
- import (
- "fmt"
- "io/ioutil"
- )
- func main() {
- fileName := "./text.txt"
- s := "Hello AlwaysBeta"
- err := ioutil.WriteFile(fileName, []byte(s), 0777)
- fmt.Println(err)
- }
TempFile
- // TempFile 在 dir 目錄中創(chuàng)建一個以 prefix 為前綴的臨時文件,并將其以讀
- // 寫模式打開。返回創(chuàng)建的文件對象和遇到的錯誤。
- // 如果 dir 為空,則在默認的臨時目錄中創(chuàng)建文件(參見 os.TempDir),多次
- // 調用會創(chuàng)建不同的臨時文件,調用者可以通過 f.Name() 獲取文件的完整路徑。
- // 調用本函數(shù)所創(chuàng)建的臨時文件,應該由調用者自己刪除。
- func TempFile(dir, prefix string) (f *os.File, err error)
舉例:
- package main
- import (
- "fmt"
- "io/ioutil"
- "os"
- )
- func main() {
- f, err := ioutil.TempFile("./", "Test")
- if err != nil {
- fmt.Println(err)
- }
- defer os.Remove(f.Name()) // 用完刪除
- fmt.Printf("%s\n", f.Name())
- }
TempDir
- package main
- import (
- "fmt"
- "io/ioutil"
- "os"
- )
- func main() {
- dir, err := ioutil.TempDir("./", "Test")
- if err != nil {
- fmt.Println(err)
- }
- defer os.Remove(dir) // 用完刪除
- fmt.Printf("%s\n", dir)
- }
NopCloser
- // NopCloser 將 r 包裝為一個 ReadCloser 類型,但 Close 方法不做任何事情。
- func NopCloser(r io.Reader) io.ReadCloser
這個函數(shù)的使用場景是這樣的:
有時候我們需要傳遞一個 io.ReadCloser 的實例,而我們現(xiàn)在有一個 io.Reader 的實例,比如:strings.Reader。
這個時候 NopCloser 就派上用場了。它包裝一個 io.Reader,返回一個 io.ReadCloser,相應的 Close 方法啥也不做,只是返回 nil。
舉例:
- package main
- import (
- "fmt"
- "io/ioutil"
- "reflect"
- "strings"
- )
- func main() {
- //返回 *strings.Reader
- reader := strings.NewReader("Hello AlwaysBeta")
- r := ioutil.NopCloser(reader)
- defer r.Close()
- fmt.Println(reflect.TypeOf(reader))
- data, _ := ioutil.ReadAll(reader)
- 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