Go語言在進行文件操做的時候,能夠有多種方法。最多見的好比直接對文件自己進行Read
和Write
; 除此以外,還可使用bufio
庫的流式處理以及分片式處理;若是文件較小,使用ioutil
也不失爲一種方法。json
面對這麼多的文件處理的方式,那麼初學者可能就會有困惑:我到底該用那種?它們之間有什麼區別?筆者試着從文件讀取來對go語言的幾種文件處理方式進行分析。數組
文件的讀取效率是全部開發者都會關心的話題,尤爲是當文件特別大的時候。爲了儘量的展現這三者對文件讀取的性能,我準備了三個文件,分別爲small.txt
,midium.txt
、large.txt
,分別對應KB級別、MB級別和GB級別。
這三個文件大小分別爲4KB、21MB、1GB。其中內容是比較常規的json
格式的文本。
測試代碼以下:app
//使用File自帶的Read func read1(filename string) int { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() buf := make([]byte, 4096) var nbytes int for { n, err := fi.Read(buf) if err != nil && err != io.EOF { panic(err) } if n == 0 { break } nbytes += n } return nbytes }
read1
函數使用的是os
庫對文件進行直接操做,爲了肯定確實都到了文件內容,並將讀到的大小字節數返回。函數
//使用bufio func read2(filename string) int { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() buf := make([]byte, 4096) var nbytes int rd := bufio.NewReader(fi) for { n, err := rd.Read(buf) if err != nil && err != io.EOF { panic(err) } if n == 0 { break } nbytes += n } return nbytes }
read2
函數使用的是bufio
庫,操做NewReader
對文件進行流式處理,和前面同樣,爲了肯定確實都到了文件內容,並將讀到的大小字節數返回。性能
//使用ioutil func read3(filename string) int { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() fd, err := ioutil.ReadAll(fi) nbytes := len(fd) return nbytes }
read3
函數是使用ioutil
庫進行文件讀取,這種方式比較暴力,直接將文件內容一次性所有讀到內存中,而後對內存中的文件內容進行相關的操做。
咱們使用以下的測試代碼進行測試:測試
func testfile1(filename string) { fmt.Printf("============test1 %s ===========\n", filename) start := time.Now() size1 := read1(filename) t1 := time.Now() fmt.Printf("Read 1 cost: %v, size: %d\n", t1.Sub(start), size1) size2 := read2(filename) t2 := time.Now() fmt.Printf("Read 2 cost: %v, size: %d\n", t2.Sub(t1), size2) size3 := read3(filename) t3 := time.Now() fmt.Printf("Read 3 cost: %v, size: %d\n", t3.Sub(t2), size3) }
在main
函數中調用以下:spa
func main() { testfile1("small.txt") testfile1("midium.txt") testfile1("large.txt") // testfile2("small.txt") // testfile2("midium.txt") // testfile2("large.txt") }
測試結果以下所示:
從以上結果可知:code
爲何會出現上面的不一樣結果?
其實ioutil
最好理解,當文件較小時,ioutil
使用ReadAll
函數將文件中全部內容直接讀入內存,只進行了一次io操做,可是os
和bufio
都是進行了屢次讀取,纔將文件處理完,因此ioutil
確定要快於os
和bufio
的。
可是隨着文件的增大,達到接近GB級別時,ioutil
直接讀入內存的弊端就顯現出來,要將GB級別的文件內容所有讀入內存,也就意味着要開闢一塊GB大小的內存用來存放文件數據,這對內存的消耗是很是大的,所以效率就慢了下來。
若是文件繼續增大,達到3GB甚至以上,ioutil
這種讀取方式就徹底無能爲力了。(一個單獨的進程空間爲4GB,真正存放數據的堆區和棧區更是遠遠小於4GB)。
而os
爲何在面對大文件時,效率會低於bufio
?經過查看bufio
的NewReader
源碼不難發現,在NewReader
裏,默認爲咱們提供了一個大小爲4096的緩衝區,因此係統調用會每次先讀取4096字節到緩衝區,而後rd.Read
會從緩衝區去讀取。blog
const ( defaultBufSize = 4096 ) func NewReader(rd io.Reader) *Reader { return NewReaderSize(rd, defaultBufSize) } func NewReaderSize(rd io.Reader, size int) *Reader { // Is it already a Reader? b, ok := rd.(*Reader) if ok && len(b.buf) >= size { return b } if size < minReadBufferSize { size = minReadBufferSize } r := new(Reader) r.reset(make([]byte, size), rd) return r }
而os
由於少了這一層緩衝區,每次讀取,都會執行系統調用,所以內核頻繁的在用戶態和內核態之間切換,而這種切換,也是須要消耗的,故而會慢於bufio
的讀取方式。
筆者翻閱網上資料,關於緩衝,有內核中的緩衝和進程中的緩衝兩種,其中,內核中的緩衝是內核提供的,即系統對磁盤提供一個緩衝區,無論有沒有提供進程中的緩衝,內核緩衝都是存在的。
而進程中的緩衝是對輸入輸出流作了必定的改進,提供的一種流緩衝,它在讀寫操做發生時,先將數據存入流緩衝中,只有當流緩衝區滿了或者刷新(如調用flush
函數)時,纔將數據取出,送往內核緩衝區,它起到了必定的保護內核的做用。
所以,咱們不難發現,os
是典型的內核中的緩衝,而bufio
和ioutil
都屬於進程中的緩衝。進程
當讀取小文件時,使用ioutil
效率明顯優於os
和bufio
,但若是是大文件,bufio
讀取會更快。
前面簡要分析了go語言三種不一樣讀取文件方式之間的區別。但實際的開發中,咱們對文件的讀取每每是以行爲單位的,即每次讀取一行進行處理。
go語言並無像C語言同樣給咱們提供好了相似於fgets
這樣的函數能夠正好讀取一行內容,所以,須要本身去實現。
從前面的對比分析能夠知道,不管是處理大文件仍是小文件,bufio
始終是最爲平滑和高效的,所以咱們考慮使用bufio
庫進行處理。
翻閱bufio
庫的源碼,發現可使用以下幾種方式進行讀取一行文件的處理:
ReadBytes
ReadString
ReadSlice
ReadLine
在討論這四種讀取一行文件操做的函數以前,仍然作一下效率測試。
測試代碼以下:
func readline1(filename string) { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() rd := bufio.NewReader(fi) for { _, err := rd.ReadBytes('\n') if err != nil || err == io.EOF { break } } } func readline2(filename string) { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() rd := bufio.NewReader(fi) for { _, err := rd.ReadString('\n') if err != nil || err == io.EOF { break } } }
func readline3(filename string) { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() rd := bufio.NewReader(fi) for { _, err := rd.ReadSlice('\n') if err != nil || err == io.EOF { break } } }
func readline4(filename string) { fi, err := os.Open(filename) if err != nil { panic(err) } defer fi.Close() rd := bufio.NewReader(fi) for { _, _, err := rd.ReadLine() if err != nil || err == io.EOF { break } } }
能夠看到,這四種操做方式,不管是函數調用,仍是函數返回值的處理,其實都是大同小異的。但經過測試效率,則能夠看出它們之間的區別。
咱們使用下面的測試代碼:
func testfile2(filename string) { fmt.Printf("============test2 %s ===========\n", filename) start := time.Now() readline1(filename) t1 := time.Now() fmt.Printf("Readline 1 cost: %v\n", t1.Sub(start)) readline2(filename) t2 := time.Now() fmt.Printf("Readline 2 cost: %v\n", t2.Sub(t1)) readline3(filename) t3 := time.Now() fmt.Printf("Readline 3 cost: %v\n", t3.Sub(t2)) readline4(filename) t4 := time.Now() fmt.Printf("Readline 4 cost: %v\n", t4.Sub(t3)) }
在main
函數中調用以下:
func main() { // testfile1("small.txt") // testfile1("midium.txt") // testfile1("large.txt") testfile2("small.txt") testfile2("midium.txt") testfile2("large.txt") }
運行結果以下所示:
經過現象,除了small.txt
以外,大體能夠分爲兩組:
ReadBytes
對小文件處理效率最差ReadLine
和ReadSlice
效率相近,要明顯快於ReadString
和ReadBytes
。爲何會出現上面的現象,不防從源碼層面進行分析。
經過閱讀源碼,咱們發現這四個函數之間存在這樣一個關係:
ReadLine
<- (調用) ReadSlice
ReadString
<- (調用)ReadBytes
<-(調用)ReadSlice
既然如此,那爲何在處理大文件時,ReadLine
效率要明顯高於ReadBytes
呢?
首先,咱們要知道,ReadSlice
是切片式讀取,即根據分隔符去進行切片。
經過源碼發下,ReadLine
只是在切片讀取的基礎上,對換行符\n
和\r\n
作了一些處理:
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) { line, err = b.ReadSlice('\n') if err == ErrBufferFull { // Handle the case where "\r\n" straddles the buffer. if len(line) > 0 && line[len(line)-1] == '\r' { // Put the '\r' back on buf and drop it from line. // Let the next call to ReadLine check for "\r\n". if b.r == 0 { // should be unreachable panic("bufio: tried to rewind past start of buffer") } b.r-- line = line[:len(line)-1] } return line, true, nil } if len(line) == 0 { if err != nil { line = nil } return } err = nil if line[len(line)-1] == '\n' { drop := 1 if len(line) > 1 && line[len(line)-2] == '\r' { drop = 2 } line = line[:len(line)-drop] } return }
而ReadBytes
則是經過append
先將讀取的內容暫存到full
數組中,最後再copy
出來,append
和copy
都是要消耗內存和io的,所以效率天然就慢了。其源碼以下所示:
func (b *Reader) ReadBytes(delim byte) ([]byte, error) { // Use ReadSlice to look for array, // accumulating full buffers. var frag []byte var full [][]byte var err error n := 0 for { var e error frag, e = b.ReadSlice(delim) if e == nil { // got final fragment break } if e != ErrBufferFull { // unexpected error err = e break } // Make a copy of the buffer. buf := make([]byte, len(frag)) copy(buf, frag) full = append(full, buf) n += len(buf) } n += len(frag) // Allocate new buffer to hold the full pieces and the fragment. buf := make([]byte, n) n = 0 // Copy full pieces and fragment in. for i := range full { n += copy(buf[n:], full[i]) } copy(buf[n:], frag) return buf, err }
讀取文件中一行內容時,ReadSlice
和ReadLine
性能優於ReadBytes
和ReadString
,但因爲ReadLine
對換行的處理更加全面(兼容\n
和\r\n
換行),所以,實際開發過程當中,建議使用ReadLine
函數。