<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 2624字,需瀏覽 6分鐘

           ·

          2022-01-03 23:43

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

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

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

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

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

          2. 文件過大的話,可能直接撐爆內(nèi)存,導致程序崩潰。

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

          下面我們正式開始。

          ioutil.ReadAll

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

          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 版本開始,直接調(diào)用 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?滿了,則追加一個元素,使其重新分配內(nèi)存
          ????????????b?=?append(b,?0)[:len(b)]
          ????????}
          ????????//?讀取內(nèi)容到?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í)行主要分三個步驟:

          1. 創(chuàng)建一個 512 字節(jié)的 buf;

          2. 不斷讀取內(nèi)容到 buf,當 buf 滿的時候,會追加一個元素,促使其重新分配內(nèi)存;

          3. 直到結尾或報錯,則返回;

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

          1. 如果期望容量大于當前容量的兩倍就會使用期望容量;

          2. 如果當前切片的長度小于 1024 就會將容量翻倍;

          3. 如果當前切片的長度大于 1024 就會每次增加 25% 的容量,直到新容量大于期望容量;

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

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

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

          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?方法,則直接調(diào)用?WriteTo
          ????if?wt,?ok?:=?src.(WriterTo);?ok?{
          ????????return?wt.WriteTo(dst)
          ????}
          ????//?同樣的,如果目標實現(xiàn)了?ReaderFrom?方法,則直接調(diào)用?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?=?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í)行步驟如下:

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

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

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

          4. 最后就是循環(huán) ReadWrite;

          對比之后就會發(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),多次
          //?調(diào)用會創(chuàng)建不同的臨時文件,調(diào)用者可以通過 f.Name()?獲取文件的完整路徑。
          //?調(diào)用本函數(shù)所創(chuàng)建的臨時文件,應該由調(diào)用者自己刪除。
          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

          // TempDir 功能同 TempFile,只不過創(chuàng)建的是目錄,返回目錄的完整路徑。
          func?TempDir(dir,?prefix?string)?(name?string,?err?error)

          舉例:

          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

          推薦閱讀:

          參考文章:

          • https://haisum.github.io/2017/09/11/golang-ioutil-readall/

          • https://juejin.cn/post/6977640348679929886

          • https://zhuanlan.zhihu.com/p/76231663


          瀏覽 179
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青草视频在线观看无码 | 国产免费操逼视频 | 日韩色情电影在线观看 | 中国三级片翔田千里老师高潮网站 | 麻豆一区在线 |