本文旨在快速介紹Go標準庫中讀取文件的許多選項。html
在Go中(就此而言,大多數底層語言和某些動態語言(如Node))返回字節流。 不將全部內容自動轉換爲字符串的好處是,其中之一是避免昂貴的字符串分配,這會增長GC壓力。數組
爲了使本文更加簡單,我將使用string(arrayOfBytes)
將bytes
數組轉換爲字符串。 可是,在發佈生產代碼時,不該將其做爲通常建議。緩存
首先,標準庫提供了多種功能和實用程序來讀取文件數據。咱們將從os軟件包中提供的基本狀況開始。這意味着兩個先決條件:bash
有了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))
}
複製代碼
雖然大多數狀況下能夠一次讀取文件,但有時咱們仍是想使用一種更加節省內存的方法。例如,以某種大小的塊讀取文件,處理它們,並重復直到結束。在下面的示例中,使用的緩衝區大小爲100字節。app
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: ", bytesread)
fmt.Println("bytestream to string: ", string(buffer[:bytesread]))
}
}
複製代碼
與徹底讀取文件相比,主要區別在於:函數
err == io.EOF
添加了特定檢查buffer [:bytesread]
。 在正常狀況下,bytesread
將與緩衝區大小相同。對於循環的每次迭代,都會更新內部文件指針。 下次讀取時,將返回從文件指針偏移開始直到緩衝區大小的數據。 該指針不是語言的構造,而是操做系統之一。 在Linux上,此指針是要建立的文件描述符的屬性。 全部的read / Read調用(分別在Ruby / Go中)在內部都轉換爲系統調用併發送到內核,而且內核管理此指針。oop
若是咱們想加快對上述塊的處理,該怎麼辦?一種方法是使用多個go例程!與串行讀取塊相比,咱們須要作的另外一項工做是咱們須要知道每一個例程的偏移量。請注意,當目標緩衝區的大小大於剩餘的字節數時,ReadAt的行爲與Read的行爲略有不一樣。性能
另請注意,我並無限制goroutine
的數量,它僅由緩衝區大小來定義。實際上,此數字可能會有上限。ui
package main
import (
"fmt"
"os"
"sync"
)
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()
fileinfo, 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()
}
複製代碼
與之前的任何方法相比,這種方法要多得多:
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)
}
}
複製代碼
bufio軟件包包含基本的預約義拆分功能:
所以,要讀取文件並在文件中建立單詞列表,可使用以下所示的內容:
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) } } 複製代碼
所以,咱們最終要進行的切片「增加」操做要少得多,但最終可能要根據緩衝區大小和文件中的單詞數在結尾處留出一些空插槽,這是一個折衷方案。
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, scanner.Text())
}
fmt.Println("word list:")
for _, word := range words {
fmt.Println(word)
}
}
複製代碼
手動解析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, atEOF bool) (advance int, token []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, nil } scanner := bufio.NewScanner(strings.NewReader(csvstring)) scanner.Split(ScanCSV) for scanner.Scan() { fmt.Println(scanner.Text()) } } 複製代碼
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))
}
}
}
複製代碼