Golang是如何讀取文件內容的

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

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

爲了使本文更加簡單,我將使用string(arrayOfBytes)bytes數組轉換爲字符串。 可是,在發佈生產代碼時,不該將其做爲通常建議。緩存

1.讀取整個文件到內存中

首先,標準庫提供了多種功能和實用程序來讀取文件數據。咱們將從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))
}
複製代碼

2.以塊的形式讀取文件

雖然大多數狀況下能夠一次讀取文件,但有時咱們仍是想使用一種更加節省內存的方法。例如,以某種大小的塊讀取文件,處理它們,並重復直到結束。在下面的示例中,使用的緩衝區大小爲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]))
	}
}

複製代碼

與徹底讀取文件相比,主要區別在於:函數

  • 讀取直到得到EOF標記,所以咱們爲err == io.EOF添加了特定檢查
  • 咱們定義了緩衝區的大小,所以咱們能夠控制所需的「塊」大小。 若是操做系統正確地將正在讀取的文件緩存起來,則能夠在正確使用時提升性能。
  • 若是文件大小不是緩衝區大小的整數倍,則最後一次迭代將僅將剩餘字節數添加到緩衝區中,所以調用buffer [:bytesread]。 在正常狀況下,bytesread將與緩衝區大小相同。

對於循環的每次迭代,都會更新內部文件指針。 下次讀取時,將返回從文件指針偏移開始直到緩衝區大小的數據。 該指針不是語言的構造,而是操做系統之一。 在Linux上,此指針是要建立的文件描述符的屬性。 全部的read / Read調用(分別在Ruby / Go中)在內部都轉換爲系統調用併發送到內核,而且內核管理此指針。oop

3.併發讀取文件塊

若是咱們想加快對上述塊的處理,該怎麼辦?一種方法是使用多個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()
}
複製代碼

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

  • 我正在嘗試建立特定數量的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, scanner.Text())
	}

	fmt.Println("word list:")
	for _, 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, 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()) } } 複製代碼

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))
		}
	}
}
複製代碼

參考文獻

相關文章
相關標籤/搜索