golang文件操作大比拼
bufio
bufio包原理 bufio 是通过缓冲来提高效率。读的时候一次多读一些,默认
io操作本身的效率并不低,低的是频繁的访问本地磁盘的文件。所以bufio就提供了缓冲区(分配一块内存),读和写都先在缓冲区中,最后再读写文件,来降低访问本地磁盘的次数,从而提高效率。
简单的说就是,把文件读取进缓冲(内存)之后再读取的时候就可以避免文件系统的io 从而提高速度。同理,在进行写操作时,先把文件写入缓冲(内存),然后由缓冲写入文件系统。看完以上解释有人可能会表示困惑了,直接把 内容->文件 和 内容->缓冲->文件相比, 缓冲区好像没有起到作用嘛。其实缓冲区的设计是为了存储多次的写入,最后一口气把缓冲区内容写入文件。
Reader对象
bufio.Reader 是bufio中对io.Reader 的封装
// Reader implements buffering for an io.Reader object.
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
bufio.Read(p []byte) 相当于读取大小len(p)的内容,思路如下:
当缓存区有内容的时,将缓存区内容全部填入p并清空缓存区 当缓存区没有内容的时候且len(p)>len(buf),即要读取的内容比缓存区还要大,直接去文件读取即可 当缓存区没有内容的时候且len(p)<len(buf),即要读取的内容比缓存区小,缓存区从文件读取内容充满缓存区,并将p填满(此时缓存区有剩余内容)
Writer对象
bufio.Writer 是bufio中对io.Writer 的封装
// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
bufio.Write(p []byte) 的思路如下
判断buf中可用容量是否可以放下 p 如果能放下,直接把p拼接到buf后面,即把内容放到缓冲区 如果缓冲区的可用容量不足以放下,且此时缓冲区是空的,直接把p写入文件即可 如果缓冲区的可用容量不足以放下,且此时缓冲区有内容,则用p把缓冲区填满,把缓冲区所有内容写入文件,并清空缓冲区 判断p的剩余内容大小能否放到缓冲区,如果能放下(此时和步骤1情况一样)则把内容放到缓冲区 如果p的剩余内容依旧大于缓冲区,(注意此时缓冲区是空的,情况和步骤3一样)则把p的剩余内容直接写入文件 以后再次读取时缓存区有内容,将缓存区内容全部填入p并清空缓存区(此时和情况1一样)
我们可以看到reader和writer都有一个buf字节切片,默认大小是4096,我们也可以自己指定大小。
文件读取比拼
os包
//使用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
}
bufio包
/使用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
}
ioutil
//使用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
}
比拼结果
当文件较小(KB 级别)时,ioutil > bufio > os。 当文件大小比较常规(MB 级别)时,三者差别不大,但 bufio 又是已经显现出来。 当文件较大(GB 级别)时,bufio > os > ioutil。
其实ioutil最好理解,当文件较小时,ioutil使用ReadAll函数将文件中所有内容直接读入内存,只进行了一次 io 操作,但是os和bufio都是进行了多次读取,才将文件处理完,所以ioutil肯定要快于os和bufio的。
但是随着文件的增大,达到接近 GB 级别时,ioutil直接读入内存的弊端就显现出来,要将 GB 级别的文件内容全部读入内存,也就意味着要开辟一块 GB 大小的内存用来存放文件数据,这对内存的消耗是非常大的,因此效率就慢了下来。如果文件继续增大,达到 3GB 甚至以上,ioutil这种读取方式就完全无能为力了。而os为什么在面对大文件时,效率会低于bufio?通过查看bufio的NewReader源码不难发现,在NewReader里,默认为我们提供了一个大小为 4096 的缓冲区,所以系统调用会每次先读取 4096 字节到缓冲区,然后rd.Read会从缓冲区去读取
os因为少了这一层缓冲区,每次读取,都会执行系统调用,因此内核频繁的在用户态和内核态之间切换,而这种切换,也是需要消耗的,故而会慢于bufio的读取方式。