1. 程式人生 > 程式設計 >淺談Golang是如何讀取檔案內容的(7種)

淺談Golang是如何讀取檔案內容的(7種)

本文旨在快速介紹Go標準庫中讀取檔案的許多選項。

在Go中(就此而言,大多數底層語言和某些動態語言(如Node))返回位元組流。 不將所有內容自動轉換為字串的好處是,其中之一是避免昂貴的字串分配,這會增加GC壓力。

為了使本文更加簡單,我將使用string(arrayOfBytes)將bytes陣列轉換為字串。 但是,在釋出生產程式碼時,不應將其作為一般建議。

1.讀取整個檔案到記憶體中

首先,標準庫提供了多種功能和實用程式來讀取檔案資料。我們將從os軟體包中提供的基本情況開始。這意味著兩個先決條件:

  • 該檔案必須容納在記憶體中
  • 我們需要預先知道檔案的大小,以便例項化一個足以容納它的緩衝區。

有了os.File物件的控制代碼,我們可以查詢大小並例項化一個位元組列表。

package main


import (
 "os"
 "fmt"
)
func main() {
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()

 fileinfo,err := file.Stat()
 if err != nil {
 fmt.Println(err)
 return
 }

 filesize := fileinfo.Size()
 buffer := make([]byte,filesize)

 bytesread,err := file.Read(buffer)
 if err != nil {
 fmt.Println(err)
 return
 }
 fmt.Println("bytes read: ",bytesread)
 fmt.Println("bytestream to string: ",string(buffer))
}

2.以塊的形式讀取檔案

雖然大多數情況下可以一次讀取檔案,但有時我們還是想使用一種更加節省記憶體的方法。例如,以某種大小的塊讀取檔案,處理它們,並重復直到結束。在下面的示例中,使用的緩衝區大小為100位元組。

package main


import (
 "io"
 "os"
 "fmt"
)

const BufferSize = 100

func main() {
 
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()

 buffer := make([]byte,BufferSize)

 for {
 bytesread,err := file.Read(buffer)
 if err != nil {
  if err != io.EOF {
  fmt.Println(err)
  }
  break
 }
 fmt.Println("bytes read: ",string(buffer[:bytesread]))
 }
}

與完全讀取檔案相比,主要區別在於:

  • 讀取直到獲得EOF標記,因此我們為err == io.EOF添加了特定檢查
  • 我們定義了緩衝區的大小,因此我們可以控制所需的“塊”大小。 如果作業系統正確地將正在讀取的檔案快取起來,則可以在正確使用時提高效能。
  • 如果檔案大小不是緩衝區大小的整數倍,則最後一次迭代將僅將剩餘位元組數新增到緩衝區中,因此呼叫buffer [:bytesread]。 在正常情況下,bytesread將與緩衝區大小相同。

對於迴圈的每次迭代,都會更新內部檔案指標。 下次讀取時,將返回從檔案指標偏移開始直到緩衝區大小的資料。 該指標不是語言的構造,而是作業系統之一。 在Linux上,此指標是要建立的檔案描述符的屬性。 所有的read / Read呼叫(分別在Ruby / Go中)在內部都轉換為系統呼叫併發送到核心,並且核心管理此指標。

3.併發讀取檔案塊

如果我們想加快對上述塊的處理,該怎麼辦?一種方法是使用多個go例程!與序列讀取塊相比,我們需要做的另一項工作是我們需要知道每個例程的偏移量。請注意,當目標緩衝區的大小大於剩餘的位元組數時,ReadAt的行為與Read的行為略有不同。

另請注意,我並沒有限制goroutine的數量,它僅由緩衝區大小來定義。實際上,此數字可能會有上限。

package main

import (
 "fmt"
 "os"
 "sync"
)

const BufferSize = 100

type chunk struct {
 bufsize int
 offset int64
}

func main() {
 
 file,err := file.Stat()
 if err != nil {
 fmt.Println(err)
 return
 }

 filesize := int(fileinfo.Size())
 // Number of go routines we need to spawn.
 concurrency := filesize / BufferSize
 // buffer sizes that each of the go routine below should use. ReadAt
 // returns an error if the buffer size is larger than the bytes returned
 // from the file.
 chunksizes := make([]chunk,concurrency)

 // All buffer sizes are the same in the normal case. Offsets depend on the
 // index. Second go routine should start at 100,for example,given our
 // buffer size of 100.
 for i := 0; i < concurrency; i++ {
 chunksizes[i].bufsize = BufferSize
 chunksizes[i].offset = int64(BufferSize * i)
 }

 // check for any left over bytes. Add the residual number of bytes as the
 // the last chunk size.
 if remainder := filesize % BufferSize; remainder != 0 {
 c := chunk{bufsize: remainder,offset: int64(concurrency * BufferSize)}
 concurrency++
 chunksizes = append(chunksizes,c)
 }

 var wg sync.WaitGroup
 wg.Add(concurrency)

 for i := 0; i < concurrency; i++ {
 go func(chunksizes []chunk,i int) {
  defer wg.Done()

  chunk := chunksizes[i]
  buffer := make([]byte,chunk.bufsize)
  bytesread,err := file.ReadAt(buffer,chunk.offset)

  if err != nil {
  fmt.Println(err)
  return
  }

  fmt.Println("bytes read,string(bytestream): ",bytesread)
  fmt.Println("bytestream to string: ",string(buffer))
 }(chunksizes,i)
 }

 wg.Wait()
}

與以前的任何方法相比,這種方法要多得多:

  • 我正在嘗試建立特定數量的Go例程,具體取決於檔案大小和緩衝區大小(在本例中為100)。
  • 我們需要一種方法來確保我們正在“等待”所有執行例程。 在此示例中,我使用的是wait group。
  • 在每個例程結束的時候,從內部發出訊號,而不是break for迴圈。因為我們延時呼叫了wg.Done(),所以在每個例程返回的時候才呼叫它。

注意:始終檢查返回的位元組數,並重新分配輸出緩衝區。

使用Read()讀取檔案可以走很長一段路,但是有時您需要更多的便利。Ruby中經常使用的是IO函式,例如each_line,each_char,each_codepoint 等等.通過使用Scanner型別以及bufio軟體包中的關聯函式,我們可以實現類似的目的。

bufio.Scanner型別實現帶有“ split”功能的函式,並基於該功能前進指標。例如,對於每個迭代,內建的bufio.ScanLines拆分函式都會使指標前進,直到下一個換行符為止.

在每個步驟中,該型別還公開用於獲取開始位置和結束位置之間的位元組陣列/字串的方法。

package main

import (
 "fmt"
 "os"
 "bufio"
)

const BufferSize = 100

type chunk struct {
 bufsize int
 offset int64
}

func main() {
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()
 scanner := bufio.NewScanner(file)
 scanner.Split(bufio.ScanLines)

 // Returns a boolean based on whether there's a next instance of `\n`
 // character in the IO stream. This step also advances the internal pointer
 // to the next position (after '\n') if it did find that token.
 for {
 read := scanner.Scan()
 if !read {
  break
  
 }
 fmt.Println("read byte array: ",scanner.Bytes())
 fmt.Println("read string: ",scanner.Text())
 }
 
}

因此,要以這種方式逐行讀取整個檔案,可以使用如下所示的內容:

package main

import (
 "bufio"
 "fmt"
 "os"
)

func main() {
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()

 scanner := bufio.NewScanner(file)
 scanner.Split(bufio.ScanLines)

 // This is our buffer now
 var lines []string

 for scanner.Scan() {
 lines = append(lines,scanner.Text())
 }

 fmt.Println("read lines:")
 for _,line := range lines {
 fmt.Println(line)
 }
}

4.逐字掃描

bufio軟體包包含基本的預定義拆分功能:

  • ScanLines (預設)
  • ScanWords
  • ScanRunes(對於遍歷UTF-8程式碼點(而不是位元組)非常有用)
  • ScanBytes

因此,要讀取檔案並在檔案中建立單詞列表,可以使用如下所示的內容:

package main

import (
 "bufio"
 "fmt"
 "os"
)

func main() {
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()

 scanner := bufio.NewScanner(file)
 scanner.Split(bufio.ScanWords)

 var words []string

 for scanner.Scan() {
 words = append(words,scanner.Text())
 }

 fmt.Println("word list:")
 for _,word := range words {
 fmt.Println(word)
 }
}

ScanBytes拆分函式將提供與早期Read()示例相同的輸出。 兩者之間的主要區別是在掃描程式中,每次需要附加到位元組/字串陣列時,動態分配問題。 可以通過諸如將緩衝區預初始化為特定長度的技術來避免這種情況,並且只有在達到前一個限制時才增加大小。 使用與上述相同的示例:

package main

import (
 "bufio"
 "fmt"
 "os"
)

func main() {
 file,err := os.Open("filetoread.txt")
 if err != nil {
 fmt.Println(err)
 return
 }
 defer file.Close()

 scanner := bufio.NewScanner(file)
 scanner.Split(bufio.ScanWords)

 // initial size of our wordlist
 bufferSize := 50
 words := make([]string,bufferSize)
 pos := 0

 for scanner.Scan() {
 if err := scanner.Err(); err != nil {
  // This error is a non-EOF error. End the iteration if we encounter
  // an error
  fmt.Println(err)
  break
 }

 words[pos] = scanner.Text()
 pos++

 if pos >= len(words) {
  // expand the buffer by 100 again
  newbuf := make([]string,bufferSize)
  words = append(words,newbuf...)
 }
 }

 fmt.Println("word list:")
 // we are iterating only until the value of "pos" because our buffer size
 // might be more than the number of words because we increase the length by
 // a constant value. Or the scanner loop might've terminated due to an
 // error prematurely. In this case the "pos" contains the index of the last
 // successful update.
 for _,word := range words[:pos] {
 fmt.Println(word)
 }
}

因此,我們最終要進行的切片“增長”操作要少得多,但最終可能要根據緩衝區大小和檔案中的單詞數在結尾處留出一些空插槽,這是一個折衷方案。

5.將長字串拆分為單詞

bufio.NewScanner使用滿足io.Reader介面的型別作為引數,這意味著它將與定義了Read方法的任何型別一起使用。
標準庫中返回reader型別的string實用程式方法之一是strings.NewReader函式。當從字串中讀取單詞時,我們可以將兩者結合起來:

package main

import (
 "bufio"
 "fmt"
 "strings"
)

func main() {
 longstring := "This is a very long string. Not."
 var words []string
 scanner := bufio.NewScanner(strings.NewReader(longstring))
 scanner.Split(bufio.ScanWords)

 for scanner.Scan() {
 words = append(words,word := range words {
 fmt.Println(word)
 }
}

6.掃描以逗號分隔的字串

手動解析CSV檔案/字串通過基本的file.Read()或者Scanner型別是複雜的。因為根據拆分功能bufio.ScanWords,“單詞”被定義為一串由unicode空間界定的符文。讀取各個符文並跟蹤緩衝區的大小和位置(例如在詞法分析中所做的工作)是太多的工作和操作。

但這可以避免。 我們可以定義一個新的拆分函式,該函式讀取字元直到讀者遇到逗號,然後在呼叫Text()或Bytes()時返回該塊。bufio.SplitFunc函式的函式簽名如下所示:

type SplitFunc func(data []byte,atEOF bool) (advance int,token []byte,err error)

為簡單起見,我展示了一個讀取字串而不是檔案的示例。 使用上述簽名的CSV字串的簡單閱讀器可以是:

package main

import (
 "bufio"
 "bytes"
 "fmt"
 "strings"
)

func main() {
 csvstring := "name,age,occupation"

 // An anonymous function declaration to avoid repeating main()
 ScanCSV := func(data []byte,err error) {
 commaidx := bytes.IndexByte(data,',')
 if commaidx > 0 {
  // we need to return the next position
  buffer := data[:commaidx]
  return commaidx + 1,bytes.TrimSpace(buffer),nil
 }

 // if we are at the end of the string,just return the entire buffer
 if atEOF {
  // but only do that when there is some data. If not,this might mean
  // that we've reached the end of our input CSV string
  if len(data) > 0 {
  return len(data),bytes.TrimSpace(data),nil
  }
 }

 // when 0,nil,nil is returned,this is a signal to the interface to read
 // more data in from the input reader. In this case,this input is our
 // string reader and this pretty much will never occur.
 return 0,nil
 }

 scanner := bufio.NewScanner(strings.NewReader(csvstring))
 scanner.Split(ScanCSV)

 for scanner.Scan() {
 fmt.Println(scanner.Text())
 }
}

7.ioutil

我們已經看到了多種讀取檔案的方式.但是,如果您只想將檔案讀入緩衝區怎麼辦?
ioutil是標準庫中的軟體包,其中包含一些使它成為單行的功能。

讀取整個檔案

package main

import (
 "io/ioutil"
 "log"
 "fmt"
)

func main() {
 bytes,err := ioutil.ReadFile("filetoread.txt")
 if err != nil {
 log.Fatal(err)
 }

 fmt.Println("Bytes read: ",len(bytes))
 fmt.Println("String read: ",string(bytes))
}

這更接近我們在高階指令碼語言中看到的內容。

讀取檔案的整個目錄

不用說,如果您有大檔案,請不要執行此指令碼

package main

import (
 "io/ioutil"
 "log"
 "fmt"
)

func main() {
 filelist,err := ioutil.ReadDir(".")
 if err != nil {
 log.Fatal(err)
 }
 for _,fileinfo := range filelist {
 if fileinfo.Mode().IsRegular() {
  bytes,err := ioutil.ReadFile(fileinfo.Name())
  if err != nil {
  log.Fatal(err)
  }
  fmt.Println("Bytes read: ",len(bytes))
  fmt.Println("String read: ",string(bytes))
 }
 }
}

參考文獻

go語言讀取檔案概述

到此這篇關於淺談Golang是如何讀取檔案內容的(7種)的文章就介紹到這了,更多相關Golang讀取檔案內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!