关于golang:Go语言文件读取的一些总结

13次阅读

共计 6322 个字符,预计需要花费 16 分钟才能阅读完成。

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

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

os.File、bufio、ioutil 比拟

效率测试

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

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

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

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 函数。

正文完
 0