当前位置: 首页 > 科技观察

为什么要避免在Go中使用ioutil.ReadAll?

时间:2023-03-12 17:22:25 科技观察

ioutil.ReadAll主要功能是从一个io.Reader中读取所有数据,直到结束。在GitHub上搜索ioutil.ReadAll,类型选择Code,语言选择Go,一共得到637,307条结果。这说明ioutil.ReadAll还是很受欢迎的,主要是它用起来确实方便。但是当遇到大文件时,这个函数会暴露出两个明显的缺点:性能问题,文件越大,性能越差。如果文件过大,可能会直接爆内存导致程序崩溃。为什么会这样?本文通过源码分析其背后的原因,并试图给出更好的解决方案。让我们现在开始。ioutil.ReadAll首先我们通过一个例子来看看ioutil.ReadAll的使用场景。例如使用http.Client发送GET请求,然后读取返回内容:funcmain(){res,err:=http.Get("http://www.google.com/robots.txt")iferr!=nil{log.Fatal(err)}robots,err:=io.ReadAll(res.Body)res.Body.Close()iferr!=nil{log.Fatal(err)}fmt.Printf("%s",robots)}http.Get()返回的数据存放在res.Body中,可以通过ioutil.ReadAll读取。从表面上看,这段代码没有任何问题,但仔细分析却并非如此。想要探究其背后的原因,只能靠源码来说话。ioutil.ReadAll源码如下://src/io/ioutil/ioutil.gofuncReadAll(rio.Reader)([]byte,error){returnio.ReadAll(r)}从Go1.16版本开始,调用io.ReadAll()直接函数,我们看io.ReadAll()的实现://src/io/io.gofuncReadAll(rReader)([]byte,error){//创建一个512字节的bufb:=make([]byte,0,512)for{iflen(b)==cap(b){//如果buf已满,添加一个元素重新分配内存b=append(b,0)[:len(b)]}//读取内容到bufn,err:=r.Read(b[len(b):cap(b)])b=b[:len(b)+n]//遇到结束或报错返回iferr!=nil{iferr==EOF{err=nil}returnb,err}}}我在代码中添加了必要的注释,这段代码的执行主要分为三步:创建一个512字节的buf;连续读取内容到buf,当buf满了,会增加一个元素,提示它重新分配内存;直到结束或报错,才会返回;知道执行步骤,但是想分析它的性能问题,还需要了解Goslice的扩容策略,如下:如果期望容量大于当前容量的两倍,则使用期望容量;如果当前分片长度小于1024,则容量翻倍;如果当前分片的长度大于1024,每次增加25%的容量,直到新的容量大于想要的容量;也就是说,如果要复制的数据的容量小于512字节,性能不会受到影响。但是如果超过512字节,就会开始分片扩容。数据量越大,扩容越频繁,对性能的影响越大。如果数据量足够大,内存可能会直接爆掉,影响很大。有更好的选择吗?当然有,我们往下看。io.Copy可以替换为io.Copy函数。源码定义如下:src/io/io.gofuncCopy(dstWriter,srcReader)(writtenint64,errerror){returncopyBuffer(dst,src,nil)}它的作用是直接从src读取数据,写入dst。与ioutil.ReadAll最大的区别在于它不是一次性取出所有数据,而是连续读写。实现Copy的逻辑在copyBuffer函数中实现://src/io/io.gofunccopyBuffer(dstWriter,srcReader,buf[]byte)(writtenint64,errerror){//如果source实现了WriteTo方法,直接调用WriteToifwt,ok:=src.(WriterTo);ok{returnwt.WriteTo(dst)}//同理,如果target实现了ReaderFrom方法,直接调用ReaderFromifrt,ok:=dst.(ReaderFrom);ok{returnrt.ReadFrom(src)}//如果buf为空,创建一个32KB的bufifbuf==nil{size:=32*1024ifl,ok:=src.(*LimitedReader);ok&&int64(size)>l.N{ifl.N<1{size=1}else{size=int(l.N)}}buf=make([]byte,size)}//循环读取数据写入for{nr,er:=src.Read(buf)ifnr>0{nw,ew:=dst.Write(buf[0:nr])ifnw<0||nr