怎麼選擇 Go 文件讀取方案

文件處理是一個常見的問題,同時 Go 又提供了非常多的文件讀取方法,容易讓人患選擇困難症。之前我們轉過一篇超全總結:Go 讀文件的 10 種方法的文章,列舉了 10 餘種讀取方式。本文作爲其擴展,以實際不同大小的文件爲例,來具體比較下它們的差異。

創建不同大小的文件

首先,我們需要有比較對象。鑑於電腦磁盤空間有限,本文就比較 KB、MB、GB 三個級別的文件讀取差異。

package main

import (
 "bufio"
 "math/rand"
 "os"
 "time"
)

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))

func StringWithCharset(length int) string {
 b := make([]byte, length)
 for i := range b {
  b[i] = charset[seededRand.Intn(len(charset))]
 }
 return string(b)
}

func main() {
 files := map[string]int{"4KB.txt": 4, "4MB.txt": 4096, "4GB.txt": 4194304, "16GB.txt": 16777216}
 for name, number := range files {
  file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
  if err != nil {
   panic(err)
  }
  write := bufio.NewWriter(file)
  for i := 0; i < number; i++ {
   s := StringWithCharset(1023) + "\n"
   write.WriteString(s)
  }
  file.Close()
 }
}

執行以上代碼,我們依次得到 4KB、4MB、4GB、16GB 大小的文件,它們是由每行 1KB 大小隨機字符串的內容組成。

$ ls -alh 4kb.txt 4MB.txt 4GB.txt 16GB.txt
-rw-r--r--  1 slp  staff    16G Mar  6 15:57 16GB.txt
-rw-r--r--  1 slp  staff   4.0G Mar  6 15:54 4GB.txt
-rw-r--r--  1 slp  staff   4.0M Mar  6 15:53 4MB.txt
-rw-r--r--  1 slp  staff   4.0K Mar  6 15:16 4kb.txt

接下來,我們使用不同的方式來讀取這些文件內容。

整個文件加載

Go 提供了可一次性讀取文件內容的方法:os.ReadFile 與 ioutil.ReadFile。在 Go 1.16 開始,ioutil.ReadFile 就等價於 os.ReadFile。

func BenchmarkOsReadFile4KB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  _, err := os.ReadFile("./4KB.txt")
  if err != nil {
   b.Fatal(err)
  }
 }
}

func BenchmarkOsReadFile4MB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  _, err := os.ReadFile("./4MB.txt")
  if err != nil {
   b.Fatal(err)
  }
 }
}

func BenchmarkOsReadFile4GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  _, err := os.ReadFile("./4GB.txt")
  if err != nil {
   b.Fatal(err)
  }
 }
}

func BenchmarkOsReadFile16GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  _, err := os.ReadFile("./16GB.txt")
  if err != nil {
   b.Fatal(err)
  }
 }
}

一次性加載文件的優缺點非常明顯,它能減少 IO 次數,但它會將文件內容都加載至內存中,對於大文件,存在內存撐爆的風險。

逐行讀取

在很多情況下,例如日誌分析,對文件的處理都是按行進行的。Go 中 bufio.Reader 對象提供了一個 ReadLine() 方法,但其實我們更多地是使用 ReadBytes('\n') 或者 ReadString('\n') 代替。

// ReadLine is a low-level line-reading primitive. Most callers should use
// ReadBytes('\n') or ReadString('\n') instead or use a Scanner.

我們以 ReadString('\n') 爲例,對 4 個文件分別進行逐行讀取

func ReadLines(filename string) {
 fi, err := os.Open(filename)
 if err != nil{
  panic(err)
 }
 defer fi.Close()
 reader := bufio.NewReader(fi)
 for {
  _, err = reader.ReadString('\n')
  if err != nil {
   if err == io.EOF {
    break
   }
   panic(err)
  }
 }
}

func BenchmarkReadLines4KB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadLines("./4KB.txt")
 }
}

func BenchmarkReadLines4MB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadLines("./4MB.txt")
 }
}

func BenchmarkReadLines4GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadLines("./4GB.txt")
 }
}

func BenchmarkReadLines16GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadLines("./16GB.txt")
 }
}

塊讀取

塊讀取也稱爲分片讀取,這也很好理解,我們可以將內容分成一塊塊的,每次讀取指定大小的塊內容。這裏,我們將塊大小設置爲 4KB。

func ReadChunk(filename string) {
 f, err := os.Open(filename)
 if err != nil {
  panic(err)
 }
 defer f.Close()
 buf := make([]byte, 4*1024)
 r := bufio.NewReader(f)
 for {
  _, err = r.Read(buf)
  if err != nil {
   if err == io.EOF {
    break
   }
   panic(err)
  }
 }
}

func BenchmarkReadChunk4KB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadChunk("./4KB.txt")
 }
}

func BenchmarkReadChunk4MB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadChunk("./4MB.txt")
 }
}

func BenchmarkReadChunk4GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadChunk("./4GB.txt")
 }
}

func BenchmarkReadChunk16GB(b *testing.B) {
 for i := 0; i < b.N; i++ {
  ReadChunk("./16GB.txt")
 }
}

彙總結果

BenchmarkOsReadFile4KB-8           92877             12491 ns/op
BenchmarkOsReadFile4MB-8            1620            744460 ns/op
BenchmarkOsReadFile4GB-8               1        7518057733 ns/op
signal: killed

BenchmarkReadLines4KB-8            90846             13184 ns/op
BenchmarkReadLines4MB-8              493           2338170 ns/op
BenchmarkReadLines4GB-8                1        3072629047 ns/op
BenchmarkReadLines16GB-8               1        12472749187 ns/op

BenchmarkReadChunk4KB-8            99848             12262 ns/op
BenchmarkReadChunk4MB-8              913           1233216 ns/op
BenchmarkReadChunk4GB-8                1        2095515009 ns/op
BenchmarkReadChunk16GB-8               1        8547054349 ns/op

在本文的測試條件下(每行數據 1KB),對於小對象 4KB 的讀取,三種方式差距並不大;在 MB 級別的讀取中,直接加載最快,但塊讀取也慢不了多少;上了 GB 後,塊讀取方式會最快。

且有一點可以注意到的是,在整個文件加載的方式中,對於 16 GB 的文件數據(測試機器運行內存爲 8GB),會內存耗盡出錯,沒法執行。

總結

不管是什麼大小的文件,均不推薦整個文件加載的方式,因爲它在小文件時的速度優勢並沒有那麼大,相較於安全隱患,不值得選擇它。

塊讀取是優先選擇,尤其對於一些沒有換行符的文件,例如音視頻等。通過設定合適的塊讀取大小,能讓速度和內存得到很好的平衡。且在讀取過程中,往往伴隨着處理內容的邏輯。每塊內容可以賦給一個工作 goroutine 來處理,能更好地併發。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8EjEiB3AJmrb7qJ5kTwatA