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