当前位置: 首页 > 后端技术 > Python

Golang中deferClose()的潜在风险

时间:2023-03-26 14:18:11 Python

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