你真的了解 defer 吗?
目录
[Go]你真的了解 defer 吗?
为什么需要 defer
defer 语法及语义
defer 使用要点
defer 函数延迟调用
defer 函数参数即时求值
反序调用

[Go]你真的了解 defer 吗?
深入理解 defer 分上下两篇文章,本文为上篇,主要介绍如下内容:
为什么需要 defer;
defer 语法及语义;
defer 使用要点;
defers 语句中的函数主要是在返回时先执行还是发生在return前?
为什么需要****defer
先来看一段没有使用 defer 的代码:
f()函数在获取资源的过程中首先成功地获得了所需资源(如打开文件、加锁操作等),随后执行了一系列非关键处理步骤。然而这些步骤可能会导致函数提前退出以避免潜在的问题因此在每次函数返回前必须确保相关资源已经被正确释放以防止可能的泄漏问题该系统中存在两个主要缺陷:代码结构较为冗杂以及软件可维护性较差。尽管冗余并非主要问题但系统的可维护性较差直接导致开发人员在面对复杂需求时往往难以高效地完成功能扩展任务特别是在修改现有功能模块时很容易出现遗漏未处理的情况从而可能导致无法预见的系统漏洞
那么我们如何解决这两个问题呢?一个可行的方案就是利用 defer 机制来触发 r.release() 函数以实现资源管理。
通过 defer 来触发 r.release() 函数的操作之后,则无需每次都预先执行 r.release() 这一操作,在代码层面确实会带来一定的简化效果。值得注意的是,在后续添加了提前返回的代码块时,“资源泄露”的问题依然不会发生原因在于无论在哪种情况下触发返回操作(return),该函数都会被自动执行。
马上码上关注我 code 杂坛 ,了解更多……
defer****语法及语义
defer语法很简单,直接在普通写法的函数调用之前加 defer 关键字即可:
defer 表示将紧跟其后的 xxx() 函数延迟执行至当前函数返回时。例如,在前文代码中标注处的 defer r.release() 表示等到后续 f() 函数返回后再调用 r.release() 。在下文中,我们将称位于 defer 语句中的函数为 **.defer函数。
defer****使用要点
对 defer 的使用需要注意如下几个要点:
延迟 对函数进行调用;
即时 对函数的参数进行求值;
根据 defer 顺序反序调用 ;
下面我们用例子来简单的看一下这几个要点。
defer****函数延迟调用
这段代码首先会输出 begin 字符串,然后是 end ,最后才输出 defer 字符串。
defer函数参数即时求值
该段代码首先执行 begin命令并传入变量i赋值为一随后执行end命令同样传入变量i赋值为二接着执行g命令并传入变量i赋值为一可以观察到g()函数会在f函数返回之后才会被调用但其接受到的参数仍然是数值一由于在defer命令所在的行中将i赋值为一也就是说defer函数会被延迟调用其接受到的参数将在该defer命令所在行处就被准备好
关注我 code 杂坛,了解更多......
反序调用
这段程序的输出如下:
可以看出,在f函数返回时,第一个defer调用是在所有其他defer调用之后完成的;然而最后一个defer调用则是在所有其他.defer调用之前执行的。
defer函数的执行与return语句之间的关系
到目前为止,defer 看起来都还比较好理解。下面我们开始把问题复杂化
输出:
这一输出相对较为直观易懂。观察到f()函数在其执行return操作前,g变量的值仍保持不变(仍为1)。因此,main函数接收到了f函数返回的结果为1。由于defer函数已经被调用,使得g变量被设置为2(已被修改)。这样,在main函数中获取到g变量的当前值仍然是2。由此可见,defer函数的作用是在return操作完成之后才执行相应的逻辑。稍作改动后可以得到以下改进版本:
输出:
通过观察输出结果可以看出,在某些情况下.defer函数改变了f函数返回的结果。然而,在前面的例子中我们发现.defer函数似乎是在return r被执行之前就进行了操作。但前面的例子表明.defer函数是在return语句之后被调用并执行的。这就产生了矛盾的结果:为什么会出现这两种不同的行为?这到底是什么原因呢?
仅就go语言而言确实较为晦涩难懂,我们有必要深入至汇编层次进行分析
老套路,使用 gdb 反汇编一下 f() 函数:
f() 函数看似简单, 但其实其内部采用了闭包机制以及Printf函数, 因此生成的汇编代码显得较为复杂. 为了便于理解, 我们重点介绍前半部分内容: f() 函数最后2条语句被编译器翻译成了如下6条汇编指令:
第一条指令对应的Go语句为r=0。因为紧随r=0之后的是return r语句,因此这一指令的作用等同于将f()函数返回值储存在栈中。随后第三条指令触发了runtime deferreturn功能,则会调用我们在f()函数开始处使用defer注册的功能来将r值更新至200。最终,在main函数中我们获取到的结果即为200;而剩下的三条指令则负责完成整个函数调用栈的操作与返回流程。
通过这几条指令可以看出,确切地说,并非是在return之后或之前执行defer函数的行为。实际上,在go语言中一个return语句会包含对defer函数的调用路径。换句话说,在编译过程中返回一条伪指令序列即可实现该功能
至此我们已经认识到,前面所说的矛盾其实并不是真正的矛盾,仅仅是从Go语言这一层次来理解就显得有些吃力.由此可见,在汇编层面前一切尽显端倪.
总结
defer旨在简化编程流程,并且用于实现panic/recover机制(后续将详细探讨这一机制)
defer 实现了函数的延迟调用;
defer 使用要点:延迟调用,即时求值和反序调用 ;
Golang中的return语句会在编译阶段被转换为一系列指令。这些指令包括保存返回地址、触发defer注册的回调函数以及负责执行函数返回操作。
本文重点阐述了.defer 以使用为导向的基本知识。
下一篇文章我们将详细探讨这两个函数体对defer 实现机制的剖析。
请关注我们的公众号 code 杂坛 ,获取更多相关信息。
