共计 6322 个字符,预计需要花费 16 分钟才能阅读完成。
Go 语言在进行文件操作的时候,能够有多种办法。最常见的比方间接对文件自身进行 Read
和Write
;除此之外,还能够应用 bufio
库的流式解决以及分片式解决;如果文件较小,应用 ioutil
也不失为一种办法。
面对这么多的文件解决的形式,那么初学者可能就会有困惑:我到底该用那种?它们之间有什么区别?笔者试着从文件读取来对 go 语言的几种文件解决形式进行剖析。
os.File、bufio、ioutil 比拟
效率测试
文件的读取效率是所有开发者都会关怀的话题,尤其是当文件特地大的时候。为了尽可能的展现这三者对文件读取的性能,我筹备了三个文件,别离为 small.txt
,midium.txt
、large.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 操作,然而 os
和bufio
都是进行了屡次读取,才将文件解决完,所以 ioutil
必定要快于 os
和bufio
的。
然而随着文件的增大,达到靠近 GB 级别时,ioutil
间接读入内存的弊病就显现出来,要将 GB 级别的文件内容全副读入内存,也就意味着要开拓一块 GB 大小的内存用来寄存文件数据,这对内存的耗费是十分大的,因而效率就慢了下来。
如果文件持续增大,达到 3GB 甚至以上,ioutil
这种读取形式就齐全无能为力了。(一个独自的过程空间为 4GB,真正存放数据的堆区和栈区更是远远小于 4GB)。
而 os
为什么在面对大文件时,效率会低于 bufio
?通过查看bufio
的NewReader
源码不难发现,在 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
是典型的内核中的缓冲,而 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
函数。