2019-03-04

Golang读取文件的四种方法运行效率对比,以及注意事项!


Golang读取文件的常见方案和效率对比,在谷歌搜索有一大堆,有的不够严谨,或者缺少实用性,有如下几个常见问题:

  • 文件读取时候产生多余字符(这个问题不易察觉,但搜索结果里面很多,复制粘贴的锅?)
  • 没有考虑append扩容和字节拼接带来的时间损耗
  • byte转string这个耗时环节有的方案用了,有的方案没用
  • 没有同时测试较大文件和较小文件

so,我决定深入研究后,重新写一篇评测记录:

+++++++++++++++分割线+++++++++++++++

读取文件主要是三种方法(我下面罗列了四组方案,最后2组是来自同一个方法)

直接说结论吧:

  • ReadCom:先os.Open(path),然后File.Read,(没给缓冲,更靠近底层一点,初测的话看起来很慢,需要优化,自己写缓冲,新手可放弃)
  • ReadBufio:先os.Open(path),然后bufio.NewReader(默认4KB缓冲,最灵活,也最复杂,需要自行根据情况优化,以适应超大文件的读取)
  • ReadIoutil:先os.Open(path),然后ioutil.ReadAll(默认512Byte缓冲,遇到大文件自动扩容,文件越大效率越低,写法比较简单)
  • ReadIoutilLite:直接ioutil.ReadFile。(这是大部分情况下能推荐的最省心,效率也最高的方案,直接给了一个文件大小+512Byte的缓冲,但不建议读取大文件,因为要注意内存消耗)

完整代码如下:

package file

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "os"
)

var (
    fi  *os.File
    err error
)

func fileClose() {
    err = fi.Close()
}

func ReadCom(path string) string {
    fi, err = os.Open(path)
    if err != nil {
        fmt.Printf("open err : %v\n", err)
    }
    defer fileClose()
    var chunks []byte    
    for {
        block := make([]byte, 1024) //这句要放在for里面(或者将后面block换做block[:readNum]),网络上搜索的不少这里没注意,会造成文件读出多余的字符
        readNum, err := fi.Read(block)        
        if err != nil && err != io.EOF {
            panic(err)
        }
        chunks = append(chunks, block...)

        if 0 == readNum {
            break
        }

    }
    return string(chunks)
}

func ReadBufio(path string) string {
    fi, err = os.Open(path)
    if err != nil {
        fmt.Printf("open err : %v\n", err)
    }
    defer fileClose()
    
    // 将文件字节流全部放入对io.Reader做了二次封装的bufio.Reader对象,设置缓冲空间大小为默认4KB
    bufReader := bufio.NewReader(fi) 
    var chunks []byte    //做一个可变容器
    //var chunks = bytes.NewBuffer(make([]byte, 1024)) //您也可以用这个做容器
    block := make([]byte, 1024) //make一个自定义大小的切片,相当于二级缓冲
    for {        
        //用io.Reader将4KB缓冲里面的字节流再次分块读取到p,这样的流程减少了io访问次数
        readNum, err := bufReader.Read(block) 
        if err != nil && err != io.EOF {
            panic(err)
        }
        if 0 == readNum {
            break
        }
        chunks = append(chunks, block[:readNum]...)
        //chunks.Write(block) //您也可以用这个做拼接,然后return chunks.String()
    }
    return string(chunks)
}

func ReadIoutil(path string) string {
    fi, err = os.Open(path)
    if err != nil {
        fmt.Printf("open err : %v\n", err)
    }
    defer fileClose()
    //直接调用了ioutil.readAll(),未计算文件大小,直接设置缓冲容器大小为512字节,最终调用底层的io.Reader
    byt, err := ioutil.ReadAll(fi) 
    if err != nil {
        fmt.Printf("ioutil read all err : %v\n", err)
    }
    return string(byt)
}

func ReadIoutilLite(path string) string {
    //先计算文件大小,再调用ioutil.readAll(),并设置缓冲容器大小 = 文件大小+512字节,速度最快,但是也最耗内存
    byt, err := ioutil.ReadFile(path) 
    if err != nil {
        fmt.Printf("open err : %v\n", err)
    }
    return string(byt)
}

计算每个函数循环10次的总时间。

func main() {
    var s string
        path := "src/file"
        t1 := time.Now()
        for i := 0; i < 10; i++ {
            _ = file.ReadCom(path + "1")
    
        }
        fmt.Printf("[ReadCom] cost time %v \n", time.Since(t1))
    
        t2 := time.Now()
        for i := 0; i < 10; i++ {
            _ = file.ReadBufio(path + "2")
        }
        fmt.Printf("[ReadBufio] cost time %v \n", time.Since(t2))    
        
        t3 := time.Now()
        for i := 0; i < 10; i++ {
            _ = file.ReadIoutil(path + "4")
    
        }
        fmt.Printf("[ReadIoutil] cost time %v \n", time.Since(t3))
    
        t4 := time.Now()
        for i := 0; i < 10; i++ {
            _ = file.ReadIoutilLite(path + "5")
        }
        fmt.Printf("[ReadIoutilLite] cost time %v \n", time.Since(t4))
    
        fmt.Printf("%s \n", s)
}

测试成绩:

如果是低于1KB的配置文件,那么各个读取函数的差别并不大,甚至会有偶尔浮动,只是ReadCom在初次读取的时候速度明显会慢点。
如下:

不相上下
[ReadCom] cost time 288.859µs 
[ReadBufio] cost time 291.319µs 
[ReadIoutil] cost time 273.937µs 
[ReadIoutilLite] cost time 324.104µs 
再来一次,下面情况还是多点
[ReadCom] cost time 316.775µs 
[ReadBufio] cost time 316.745µs 
[ReadIoutil] cost time 274.387µs 
[ReadIoutilLite] cost time 276.21µs 

14KB的配置文件,开始能看到差距了,ReadCom速度总是最慢的
如下:

[ReadCom] cost time 907.104µs 
[ReadBufio] cost time 883.849µs 
[ReadIoutil] cost time 613.267µs 
[ReadIoutilLite] cost time 418.175µs 

如果是100MB的大文件,差距已经很明显了,而且值得注意的是用block := make([]byte, 1024)设置block切片大小的时候,越靠近文件大小,Bufio和ReadCom的速度越快,甚至接近Ioutil方案。
如下:

[ReadCom] cost time 2.636320426s 
[ReadBufio] cost time 1.550074715s 
[ReadIoutil] cost time 510.43783ms 
[ReadIoutilLite] cost time 313.291665ms 

但是做下修改:block := make([]byte, 1024*1024*100)之后呢?

[ReadCom] cost time 604.839308ms 
[ReadBufio] cost time 463.107829ms 
[ReadIoutil] cost time 593.417837ms 
[ReadIoutilLite] cost time 303.74246ms 

1GB以上,怕机器撑不住就没测了。

总结:

其实无论那种方案,读取的原理都差不多,基本是靠缓冲来加速,但有一个影响效率的不可忽视的关键:由于ReadBufio和ReadCom需要自己写for循环读缓冲,然后用append或者bytes.Buffer对Byte字符进行拼接,而这个拼接过程其实会明显拖慢时间(除非你只是测试,不真正的读取文件内容),所以你需要根据内存占用,文件大小和读取频率需求做针对性优化,否则效率要低很多。

对于常见的1MB以内的配置文件,等你优化完之后,会发现还不如直接使用ioutil.ReadFile来得省心和高效。

以上,个人原创,可能还有更优方案。