Go语言文件读取的一些总结
作者:禹鼎侯 发布时间:2024-04-27 15:30:45
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函数。
来源:https://segmentfault.com/a/1190000023691973
猜你喜欢
- 问题描述:两个 go 程轮流打印一个切片。Golang 实现:使用两个 channel,只用来判断package mainimport (
- javascript单线程JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及
- i前端:nput_test.html<!DOCTYPE html><html><head lang="
- 常用功能 mean(data)mean(data)用于求给定序列或者迭代器的算术平均数。import statisticsexample_l
- PHP添加图像处理(ImageMagick)下载地址:http://pecl.php.net/package/imagick安装说明:htt
- 自动更新统计信息的基本算法是: · 如果表格是在 tempdb 数据库表的基数是小于 6,自动更新到表的每个六个修改。 · 如果表的基数是大
- Python 语言与 Perl,C 和 Java 等语言有许多相似之处,也有一定的差异性,以下是Python语言获取文件后缀名和文件名的方法
- 1. lock互斥锁知识点:lock.acquire()# 上锁lock.release()# 解锁#同一时间允许一个进程上一把锁 就是Lo
- 本文实例讲述了python使用Queue在多个子进程间交换数据的方法。分享给大家供大家参考。具体如下:这里将Queue作为中间通道进行数据传
- Eloquent: 关联模型简介数据库中的表经常性的关联其它的表。比如,一个博客文章可以有很多的评论,或者一个订单会关联一个用户。Eloqu
- 文中为大家分享了三种JavaScript判断对象是否为数组的方法,1. typeof首先我们会想到的是使用typeof来检测数据类型,但是对
- 使用thinkphp官方的WeChat包,使用不同模式可以成功,但是安全模式就是不行,现将分析解决结果做下记录。分析问题: &nb
- 引言使用 python 绘制网络训练过程中的的 loss 曲线以及准确率变化曲线,这里的主要思想就时先把想要的损失值以及准确率值保存下来,保
- 前言过年了,家家户户都得贴春联,红红火火过大年~春联是天朝传统节日完美衔接了民族文化的产物,以美好的诗词文字表达美好愿望,是 * 有文学形式
- 阅读上一篇:javascript面向对象编程(二) [Interface,Class.implement 接口及实现]接口规定了一些方法,如
- 看了一段时间关于js原型的知识,js的扩展方法是基于原型的,如Array.prototype.XXXX就是给Array扩展XXX方法,然后数
- 不知道您是否留意了,浏览本站时,浏览器右下角有一个标着top的黑色直角三角形,可以点击它返回到正在浏览的网页页眉。当滚动网页时,它的位置一直
- 由于计算机软件的非法复制,通信的泄密、数据安全受到威胁。一般为了安全,会要求将数据库名称、密码等信息进行加密。所以加密在开发过程中是经常使用
- 引子之前clubot使用的pyxmpp2的默认mainloop也就是一个poll的主循环,但是clubot上线后资源占用非常厉害,使用str
- 这篇文章主要介绍了Python matplotlib画曲线例题解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价