百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Golang 中 defer Close() 的潜在风险

bigegpt 2024-08-24 23:10 2 浏览

作为一名 Gopher,我们很容易形成一个编程惯例:每当有一个实现了 io.Closer 接口的对象 x 时,在得到对象并检查错误之后,会立即使用 defer x.Close() 以保证函数返回时 x 对象的关闭 。以下给出两个惯用写法例子。

  • HTTP 请求
resp, err := http.Get("https://golang.google.cn/")
if err != nil {
    return err
}
defer resp.Body.Close()
// The following code: handle resp
  • 访问文件
f, err := os.Open("/home/golangshare/gopher.txt")
if err != nil {
    return err
}
defer f.Close()
// The following code: handle f

存在问题

实际上,这种写法是存在潜在问题的。defer x.Close() 会忽略它的返回值,但在执行 x.Close() 时,我们并不能保证 x 一定能正常关闭,万一它返回错误应该怎么办?这种写法,会让程序有可能出现非常难以排查的错误。

那么,Close() 方法会返回什么错误呢?在 POSIX 操作系统中,例如 Linux 或者 maxOS,关闭文件的 Close() 函数最终是调用了系统方法 close(),我们可以通过 man close 手册,查看 close() 可能会返回什么错误

ERRORS
     The close() system call will fail if:

     [EBADF]            fildes is not a valid, active file descriptor.

     [EINTR]            Its execution was interrupted by a signal.

     [EIO]              A previously-uncommitted write(2) encountered an
                        input/output error.

错误 EBADF 表示无效文件描述符 fd,与本文中的情况无关;EINTR 是指的 Unix 信号打断;那么本文中可能存在的错误是 EIO

EIO 的错误是指未提交读,这是什么错误呢?

EIO 错误是指文件的 write() 的读还未提交时就调用了 close() 方法。

上图是一个经典的计算机存储器层级结构,在这个层次结构中,从上至下,设备的访问速度越来越慢,容量越来越大。存储器层级结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。

CPU 访问寄存器会非常之快,相比之下,访问 RAM 就会很慢,而访问磁盘或者网络,那意味着就是蹉跎光阴。如果每个 write() 调用都将数据同步地提交到磁盘,那么系统的整体性能将会极度降低,而我们的计算机是不会这样工作的。当我们调用 write() 时,数据并没有立即被写到目标载体上,计算机存储器每层载体都在缓存数据,在合适的时机下,将数据刷到下一层载体,这将写入调用的同步、缓慢、阻塞的同步转为了快速、异步的过程。

这样看来,EIO 错误的确是我们需要提防的错误。这意味着如果我们尝试将数据保存到磁盘,在 defer x.Close() 执行时,操作系统还并未将数据刷到磁盘,这时我们应该获取到该错误提示(只要数据还未落盘,那数据就没有持久化成功,它就是有可能丢失的,例如出现停电事故,这部分数据就永久消失了,且我们会毫不知情)。但是按照上文的惯例写法,我们程序得到的是 nil 错误。

解决方案

我们针对关闭文件的情况,来探讨几种可行性改造方案

  • 第一种方案,那就是不使用 defer
func solution01() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }

    if _, err = io.WriteString(f, "hello gopher"); err != nil {
        f.Close()
        return err
    }

    return f.Close()
}

这种写法就需要我们在 io.WriteString 执行失败时,明确调用 f.Close() 进行关闭。但是这种方案,需要在每个发生错误的地方都要加上关闭语句 f.Close(),如果对 f 的写操作 case 较多,容易存在遗漏关闭文件的风险。

  • 第二种方案是,通过命名返回值 err 和闭包来处理
func solution02() (err error) {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return
    }

    defer func() {
        closeErr := f.Close()
        if err == nil {
            err = closeErr
        }
    }()

    _, err = io.WriteString(f, "hello gopher")
    return
}

这种方案解决了方案一中忘记关闭文件的风险,如果有更多 if err !=nil 的条件分支,这种模式可以有效降低代码行数。

  • 第三种方案是,在函数最后 return 语句之前,显示调用一次 f.Close()
func solution03() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err := io.WriteString(f, "hello gopher"); err != nil {
        return err
    }

    if err := f.Close(); err != nil {
        return err
    }
    return nil
}

这种解决方案能在 io.WriteString 发生错误时,由于 defer f.Close() 的存在能得到 close 调用。也能在 io.WriteString 未发生错误,但缓存未刷新到磁盘时,得到 err := f.Close() 的错误,而且由于 defer f.Close() 并不会返回错误,所以并不担心两次 Close() 调用会将错误覆盖。

  • 最后一种方案是,函数 return 时执行 f.Sync()
func solution04() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err = io.WriteString(f, "hello world"); err != nil {
        return err
    }

    return f.Sync()
}

由于调用 close() 是最后一次获取操作系统返回错误的机会,但是在我们关闭文件时,缓存不一定被会刷到磁盘上。那么,我们可以调用 f.Sync() (其内部调用系统函数 fsync )强制性让内核将缓存持久到磁盘上去。

// Sync commits the current contents of the file to stable storage.
// Typically, this means flushing the file system's in-memory copy
// of recently written data to disk.
func (f *File) Sync() error {
    if err := f.checkValid("sync"); err != nil {
        return err
    }
    if e := f.pfd.Fsync(); e != nil {
        return f.wrapErr("sync", e)
    }
    return nil
}

由于 fsync 的调用,这种模式能很好地避免 close 出现的 EIO。可以预见的是,由于强制性刷盘,这种方案虽然能很好地保证数据安全性,但是在执行效率上却会大打折扣。

相关推荐

AI「自我复制」能力曝光,RepliBench警示:大模型正在学会伪造身份

科幻中AI自我复制失控场景,正成为现实世界严肃的研究课题。英国AISI推出RepliBench基准,分解并评估AI自主复制所需的四大核心能力。测试显示,当前AI尚不具备完全自主复制能力,但在获取资源...

【Python第三方库安装】介绍8种情况,这里最全看这里就够了!

**本图文作品主要解决CMD或pycharm终端下载安装第三方库可能出错的问题**本作品介绍了8种安装方法,这里最全的python第三方库安装教程,简单易上手,满满干货!希望大家能愉快地写代码,而不要...

pyvips,一个神奇的 Python 库!(pythonvip视频)

大家好,今天为大家分享一个神奇的Python库-pyvips。在图像处理领域,高效和快速的图像处理工具对于开发者来说至关重要。pyvips是一个强大的Python库,基于libvips...

mac 安装tesseract、pytesseract以及简单使用

一.tesseract-OCR的介绍1.tesseract-OCR是一个开源的OCR引擎,能识别100多种语言,专门用于对图片文字进行识别,并获取文本。但是它的缺点是对手写的识别能力比较差。2.用te...

实测o3/o4-mini:3分钟解决欧拉问题,OpenAI最强模型名副其实!

号称“OpenAI迄今为止最强模型”,o3/o4-mini真实能力究竟如何?就在发布后的几小时内,网友们的第一波实测已新鲜出炉。最强推理模型o3,即使遇上首位全职提示词工程师RileyGoodsid...

使用Python将图片转换为字符画并保存到文件

字符画(ASCIIArt)是将图片转换为由字符组成的艺术作品。利用Python,我们可以轻松实现图片转字符画的功能。本教程将带你一步步实现这个功能,并详细解释每一步的代码和实现原理。环境准备首先,你...

5分钟-python包管理器pip安装(python pip安装包)

pip是一个现代的,通用、普遍的Python包管理工具。提供了对Python包的查找、下载、安装、卸载的功能,是Python开发的基础。第一步:PC端打开网址:选择gz后缀的文件下载第二步:...

网络问题快速排查,你也能当好自己家的网络攻城狮

前面写了一篇关于网络基础和常见故障排查的,只列举了工具。没具体排查方式。这篇重点把几个常用工具的组合讲解一下。先有请今天的主角:nslookup及dig,traceroute,httping,teln...

终于把TCP/IP 协议讲的明明白白了,再也不怕被问三次握手了

文:涤生_Woo下周就开始和大家成体系的讲hadoop了,里面的每一个模块的技术细节我都会涉及到,希望大家会喜欢。当然了你也可以评论或者留言自己喜欢的技术,还是那句话,希望咱们一起进步。今天周五,讲讲...

记一次工控触摸屏故障的处理(工控触摸屏维修)

先说明一下,虽然我是自动化专业毕业,但已经很多年不从事现场一线的工控工作了。但自己在单位做的工作也牵涉到信息化与自动化的整合,所以平时也略有关注。上一周一个朋友接到一个活,一家光伏企业用于启动机组的触...

19、90秒快速“读懂”路由、交换命令行基础

命令行视图VRP分层的命令结构定义了很多命令行视图,每条命令只能在特定的视图中执行。本例介绍了常见的命令行视图。每个命令都注册在一个或多个命令视图下,用户只有先进入这个命令所在的视图,才能运行相应的命...

摄像头没图像的几个检查方法(摄像头没图像怎么修复)

背景描述:安防监控项目上,用户的摄像头运行了一段时间有部分摄像头不能进行预览,需要针对不能预览的摄像头进行排查,下面列出几个常见的排查方法。问题解决:一般情况为网络、供电、设备配置等情况。一,网络检查...

小谈:必需脂肪酸(必需脂肪酸主要包括)

必需脂肪酸是指机体生命活动必不可少,但机体自身又不能合成,必需由食物供给的多不饱和脂肪酸(PUFA)。必需脂肪酸主要包括两种,一种是ω-3系列的α-亚麻酸(18:3),一种是ω-6系列的亚油酸(18:...

期刊推荐:15本sci四区易发表的机械类期刊

  虽然,Sci四区期刊相比收录在sci一区、二区、三区的期刊来说要求不是那么高,投稿起来也相对容易一些。但,sci四区所收录的期刊中每本期刊的投稿难易程度也是不一样的。为方便大家投稿,本文给大家推荐...

be sick of 用法考察(be in lack of的用法)

besick表示病了,做谓语.本身是形容词,有多种意思.最通常的是:生病,恶心,呕吐,不适,晕,厌烦,无法忍受asickchild生病的孩子Hermother'sverysi...