Go语言在进行文件操作的时候,能够有多种办法。最常见的比方间接对文件自身进行ReadWrite; 除此之外,还能够应用bufio库的流式解决以及分片式解决;如果文件较小,应用ioutil也不失为一种办法。

面对这么多的文件解决的形式,那么初学者可能就会有困惑:我到底该用那种?它们之间有什么区别?笔者试着从文件读取来对go语言的几种文件解决形式进行剖析。

os.File、bufio、ioutil比拟

效率测试

文件的读取效率是所有开发者都会关怀的话题,尤其是当文件特地大的时候。为了尽可能的展现这三者对文件读取的性能,我筹备了三个文件,别离为small.txtmidium.txtlarge.txt,别离对应KB级别、MB级别和GB级别。

这三个文件大小别离为4KB、21MB、1GB。其中内容是比拟惯例的json格局的文本。
测试代码如下:

//应用File自带的Readfunc 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库对文件进行间接操作,为了确定的确都到了文件内容,并将读到的大小字节数返回。

//应用bufiofunc 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对文件进行流式解决,和后面一样,为了确定的确都到了文件内容,并将读到的大小字节数返回。

//应用ioutilfunc 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函数中调用如下:

func main() {    testfile1("small.txt")    testfile1("midium.txt")    testfile1("large.txt")    // testfile2("small.txt")    // testfile2("midium.txt")    // testfile2("large.txt")}

测试后果如下所示:

从以上后果可知:

  • 当文件较小(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会从缓冲区去读取。

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")}

运行后果如下所示:

通过景象,除了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函数。