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

你有考虑过Defer Close() 的风险吗

时间:2023-03-15 16:09:32 科技观察

您是否考虑过DeferClose()的风险?转载本文请联系Golang技术分享公众号。作为一个Gopher,我们很容易形成一个编程习惯:每当有一个实现了io.Closer接口的对象x,在拿到对象并检查错误后,我们会立即使用deferx.Close()来确保该函数返回x对象的闭包。下面给出两个成语的例子。HTTP请求1resp,err:=http.Get("https://golang.google.cn/")2iferr!=nil{3returnerr4}5deferresp.Body.Close()6//下面的代码:handleresp访问文件1f,err:=os.Open("/home/golangshare/gopher.txt")2iferr!=nil{3returnerr4}5deferf.Close()6//下面的代码:handlef有问题其实这个里面有潜在的问题写作方式。deferx.Close()会忽略它的返回值,但是当x.Close()执行时,我们不能保证x能正常关闭。如果它返回错误,我们该怎么办?这种写法会使程序有非常难排错的可能发生。那么,Close()方法会返回什么错误呢?在POSIX操作系统中,比如Linux或者maxOS,关闭文件的Close()函数最后调用的是系统方法close()。我们可以检查close()可能返回什么错误1ERRORS2Theclose()systemcallwillfailif:34[EBADF]fildesisnotvalid,activefiledescriptor.56[EINTR]Itsexecutionwasinterruptedbyasignal.78[EIO]Apreviously-uncommittedwrite(2)encounteredan9input/outputerror.ErrorEBADF表示无效的文本文件描述符fd,与中的情况无关;EINTR指的是Unix信号中断;那么这篇文章中可能的错误就是EIO。EIO的错误是指未提交的读。这是什么错误?计算机存储层级的EIO错误是指在文件的read或write()还没有提交的情况下调用close()方法。上图是一个经典的计算机内存层次结构,从上到下,设备的访问速度越来越慢,容量越来越大。内存层次结构的主要思想是上层内存作为下层内存的缓存。CPU访问寄存器会非常快。相比之下,访问RAM会很慢,而访问磁盘或网络意味着时间被浪费了。如果每次write()调用都将数据同步提交到磁盘,那么系统的整体性能将会极度下降,而我们的计算机不会那样工作。当我们调用write()时,数据并没有立即写入目标载体。计算机内存的每一层都在缓存数据。在合适的时候,数据被flush到下一层的carrier,写入到所谓的Synchronous,慢速,阻塞同步变成快速,异步的过程。这样看来,EIO错误确实是我们需要警惕的错误。这意味着如果我们尝试将数据保存到磁盘,当deferx.那么数据就不会持久化成功,有可能会丢失。例如,如果停电,这部分数据将永久消失,我们不会知道)。但是根据上面的约定,我们的程序得到了一个nil错误。解决方案下面我们探讨几种可行的关闭文件改造方案。第一种方案是不使用defer1funcsolution01()error{2f,err:=os.Create("/home/golangshare/gopher.txt")3iferr!=nil{4returnerr5}67if_,err=io.WriteString(f,"hellologopher");err!=nil{8f.Close()9returnerr10}1112returnf.Close()13}这种写法需要我们在.WriteString失败时,显式调用f.Close()关闭。但是,在这个解决方案中,需要在每个出错的地方添加一个关闭语句f.Close()。如果对f的写操作较多,则存在文件close丢失的风险。第二种方案是通过命名返回值err和闭包来处理1funcsolution02()(errerror){2f,err:=os.Create("/home/golangshare/gopher.txt")3iferr!=nil{4return5}67deferfunc(){8closeErr:=f.Close()9iferr==nil{10err=closeErr11}12}()1314_,err=io.WriteString(f,"hellologopher")15return16}这个方案解决了方案一中的遗忘问题关闭文件的风险,如果iferr!=nil条件分支比较多,这种模式可以有效减少代码行数。第三种方案是在函数最后一条return语句之前显示调用f.Close()1funcsolution03()error{2f,err:=os.Create("/home/golangshare/gopher.txt")3iferr!=nil{4returnerr5}6deferf.Close()78if_,err:=io.WriteString(f,"hellologopher");err!=nil{9returnerr10}1112iferr:=f.Close();err!=nil{13returnerr14}15returnil16}这个当io.WriteString发生错误时,由于deferf.Close()的存在,该解决方案可以得到closecall。你也可以在io.WriteString没有错误的时候得到err:=f.Close()错误,但是缓存没有刷新到磁盘,由于deferf.Close()没有返回错误,你不用担心关于它两次Close()调用将覆盖错误。最后的解决方案是在函数返回时执行f.Sync()1funcsolution04()error{2f,err:=os.Create("/home/golangshare/gopher.txt")3iferr!=nil{4returnerr5}6deferf.Close()78if_,err=io.WriteString(f,"helloworld");err!=nil{9returnerr10}1112returnf.Sync()13}由于调用close()是获得操作系统返回错误的最后机会,但是在我们关闭文件的时候,缓存并不一定会刷新到磁盘。然后,我们可以调用f.Sync()(它在内部调用系统函数fsync)强制内核将缓存持久化到磁盘。1//Sync将当前内容作为文件提交到稳定存储。2//通常,这意味着将文件系统的内存副本3//最近写入的数据刷新到磁盘。4func(f*File)Sync()错误{5iferr:=f.checkValid("sync");err!=nil{6returnerr7}8ife:=f.pfd.Fsync();e!=nil{9returnf.wrapErr("sync",e)10}11returnnil12}由于fsync的调用,这种模式可以很好的避免close中出现的EIO。可以预见,由于强制刷写,该方案虽然可以很好地保证数据安全,但会大大降低执行效率。