Go语言文件读取的一些总结

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

到此这篇关于Go语言文件读取的一些总结的文章就介绍到这了,更多相关Go语言文件读取内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • GO语言常用的文件读取方式

    本文实例讲述了GO语言常用的文件读取方式.分享给大家供大家参考.具体分析如下: Golang 的文件读取方法很多,刚上手时不知道怎么选择,所以贴在此处便后速查. 一次性读取 小文件推荐一次性读取,这样程序更简单,而且速度最快. 复制代码 代码如下: func ReadAll(filePth string) ([]byte, error) {  f, err := os.Open(filePth)  if err != nil {   return nil, err  } return iouti

  • go语言读取csv文件并输出的方法

    本文实例讲述了go语言读取csv文件并输出的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: package main import (     "encoding/csv"     "fmt"     "io"     "os" ) func main() {     file, err := os.Open("names.txt")     if err != nil {      

  • Go语言文件读取的一些总结

    Go语言在进行文件操作的时候,可以有多种方法.最常见的比如直接对文件本身进行Read和Write: 除此之外,还可以使用bufio库的流式处理以及分片式处理:如果文件较小,使用ioutil也不失为一种方法. 面对这么多的文件处理的方式,那么初学者可能就会有困惑:我到底该用那种?它们之间有什么区别?笔者试着从文件读取来对go语言的几种文件处理方式进行分析. os.File.bufio.ioutil比较 效率测试 文件的读取效率是所有开发者都会关心的话题,尤其是当文件特别大的时候.为了尽可能的展示这

  • 一文带你掌握Go语言中的文件读取操作

    目录 os 包 和 bufio 包 os.Open 与 os.OpenFile 以及 File.Read 读取文件操作 bufio.NewReader 和 Reader.ReadString 读取文件操作 小结 os 包 和 bufio 包 Go 标准库的 os 包,为我们提供很多操作文件的函数,如 Open(name) 打开文件.Create(name) 创建文件等函数,与之对应的是 bufio 包,os 包是直接对磁盘进行操作的,而 bufio 包则是带有缓冲的操作,不用每次都去操作磁盘.

  • PHP实现类似于C语言的文件读取及解析功能

    本文实例讲述了PHP实现类似于C语言的文件读取及解析功能.分享给大家供大家参考,具体如下: $log_file_name = 'D:/static/develop/kuai_zhi/acagrid.com/public/Logs/'.date('Ym').'/'.date('d').'_error.log'; //$log_file_name = 'D:/static/develop/kuai_zhi/acagrid.com/public/Logs/201701/19_error.log'; i

  • 从C语言中读取Python 类文件对象

    问题 你要写C扩展来读取来自任何Python类文件对象中的数据(比如普通文件.StringIO对象等). 解决方案 要读取一个类文件对象的数据,你需要重复调用 read() 方法,然后正确的解码获得的数据. 下面是一个C扩展函数例子,仅仅只是读取一个类文件对象中的所有数据并将其输出到标准输出: #define CHUNK_SIZE 8192 /* Consume a "file-like" object and write bytes to stdout */ static PyObj

  • R语言批量读取某路径下文件内容的方法

    R刚入门的时候,能够正确读取单个文件就觉得小有成就,随着时间的积累,单一文件地读取已经不能满足需求了,此时,批量地做就是解放双手地过程. 使用for循环把下载地TCGA数据读入R语言并转换成数据框 使用三个for循环来完成,这是第一个for循环. 1. 把所有数据读入在一个文件夹中 dir.create("data_in_one") #创建目标文件夹,也可右键创建 dir("rawdata/") #查看原路径的内容 for (dirname in dir("

  • Go语言文件操作的方法

    本文实例讲述了Go语言文件操作的方法.分享给大家供大家参考.具体如下: 关闭文件: 复制代码 代码如下: func (file *File) Close() os.Error {     if file == nil {         return os.EINVAL     }     e := syscall.Close(file.fd)     file.fd = -1 // so it can't be closed again     if e != 0 {         retu

  • fastcgi文件读取漏洞之python扫描脚本

    PHP FastCGI的远程利用 说到FastCGI,大家都知道这是目前最常见的webserver动态脚本执行模型之一.目前基本所有web脚本都基本支持这种模式,甚至有的类型脚本这是唯一的模式(ROR,Python等). FastCGI的主要目的就是,将webserver和动态语言的执行分开为两个不同的常驻进程,当webserver接收到动态脚本的请求,就通过fcgi协议将请求通过网络转发给fcgi进程,由fcgi进程进行处理之后,再将结果传送给webserver,然后webserver再输出给

  • C语言 文件的随机读写详解及示例代码

    前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据.但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写.这种读写方式称为随机读写,也就是说从文件的任意位置开始读写. 实现随机读写的关键是要按要求移动位置指针,这称为文件的定位. 文件定位函数rewind和fseek 移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek(). rewind() 用来将位置指针移动到文件开头,前面已经多次使用过,它的原型为

  • C语言 文件操作解析详解及实例代码

    C语言文件操作解析 在文件操作中除了打开操作以及读写操作,还有几种比较常见的操作.下面介绍一下这些操作中涉及到的函数. 一.移动位置指针的函数 rewind函数和fseek函数,这两个函数的原型是: void rewind(FILE *fp);     将位置指针移动到文件首 int fseek(FILE *fp,long int offset,int origin);   将位置指针移动到距离origin的offset字节数的位置 其中对于fseek函数中的参数,origin为起始点,offs

随机推荐