Go語言文件讀取的一些總結

Go語言在進行文件操做的時候,能夠有多種方法。最多見的好比直接對文件自己進行ReadWrite; 除此以外,還可使用bufio庫的流式處理以及分片式處理;若是文件較小,使用ioutil也不失爲一種方法。json

面對這麼多的文件處理的方式,那麼初學者可能就會有困惑:我到底該用那種?它們之間有什麼區別?筆者試着從文件讀取來對go語言的幾種文件處理方式進行分析。數組

os.File、bufio、ioutil比較

效率測試

文件的讀取效率是全部開發者都會關心的話題,尤爲是當文件特別大的時候。爲了儘量的展現這三者對文件讀取的性能,我準備了三個文件,分別爲small.txtmidium.txtlarge.txt,分別對應KB級別、MB級別和GB級別。
image.png
這三個文件大小分別爲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")
}

測試結果以下所示:
image.png
從以上結果可知:code

  • 當文件較小(KB級別)時,ioutil > bufio > os。
  • 當文件大小比較常規(MB級別)時,三者差異不大,但bufio又是已經顯現出來。
  • 當文件較大(GB級別)時,bufio > os > ioutil。

緣由分析

爲何會出現上面的不一樣結果?
其實ioutil最好理解,當文件較小時,ioutil使用ReadAll函數將文件中全部內容直接讀入內存,只進行了一次io操做,可是osbufio都是進行了屢次讀取,纔將文件處理完,因此ioutil確定要快於osbufio的。
可是隨着文件的增大,達到接近GB級別時,ioutil直接讀入內存的弊端就顯現出來,要將GB級別的文件內容所有讀入內存,也就意味着要開闢一塊GB大小的內存用來存放文件數據,這對內存的消耗是很是大的,所以效率就慢了下來。
若是文件繼續增大,達到3GB甚至以上,ioutil這種讀取方式就徹底無能爲力了。(一個單獨的進程空間爲4GB,真正存放數據的堆區和棧區更是遠遠小於4GB)。
os爲何在面對大文件時,效率會低於bufio?經過查看bufioNewReader源碼不難發現,在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是典型的內核中的緩衝,而bufioioutil都屬於進程中的緩衝。進程

總結

當讀取小文件時,使用ioutil效率明顯優於osbufio,但若是是大文件,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")
}

運行結果以下所示:
image.png

經過現象,除了small.txt以外,大體能夠分爲兩組:

  • ReadBytes對小文件處理效率最差
  • 在處理大文件時,ReadLineReadSlice效率相近,要明顯快於ReadStringReadBytes

緣由分析

爲何會出現上面的現象,不防從源碼層面進行分析。
經過閱讀源碼,咱們發現這四個函數之間存在這樣一個關係:

  • 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出來,appendcopy都是要消耗內存和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
}

總結

讀取文件中一行內容時,ReadSliceReadLine性能優於ReadBytesReadString,但因爲ReadLine對換行的處理更加全面(兼容\n\r\n換行),所以,實際開發過程當中,建議使用ReadLine函數。

相關文章
相關標籤/搜索