什么是defer

<font style="color:#2D3037;">golang</font>中,我们使用<font style="color:#2D3037;">defer</font>语句来进行一些错误处理和收尾工作,它的作用类似<font style="color:#2D3037;">java</font>里面<font style="color:#2D3037;">finally</font>关键字的作用

defer,即延迟调用,是Go语言的一大特色。defer代码块会在函数调用链表中增加一个函数调用,在函数正常返回,即return返回之后,增加一个函数调用。因此,defer常用来回收资源,哪怕程序执行有错误,依然能够保证回收资源等操作能够执行。

为什么需要defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完 毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。

defer快速入门

<font style="color:#333333;">defer</font>后面必须是函数调用语句,不能是其他语句,否则编译器会出错

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer fmt.Println("world")
  5. fmt.Println("hello")
  6. }

输出结果

  1. hello
  2. world

defer 规则

我们通过以下代码来解释这条规则:

  1. func a() {
  2. i := 0
  3. defer fmt.Println(i)
  4. i++
  5. return
  6. }

上面我们说过,defer 函数会在 return 之后被调用。那么这段函数执行完之后,是不用应该输出 1 呢?

读者自行编译看一下,结果输出的是 0. why?

:::info 这是因为虽然我们在 defer 后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量 (i) 在 defer 被声明的时候,就已经确定其确定的值了。

:::

换言之,上面的代码等同于下面的代码:

  1. func a() {
  2. i := 0
  3. defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
  4. i++
  5. return
  6. }

为了更为明确的说明这个问题,我们继续定义一个 defer:

  1. func a() {
  2. i := 0
  3. defer fmt.Println(i) //输出0,因为i此时就是0
  4. i++
  5. defer fmt.Println(i) //输出1,因为i此时就是1
  6. return
  7. }

通过运行结果,可以看到 defer 输出的值,就是定义时的值。而不是 defer 真正执行时的变量值 (很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出 1,在输出 0 呢? 看下面的规则二。

规则二 defer 执行顺序为先进后出

当同时定义了多个 defer 代码块时,golang 安装先定义后执行的顺序依次调用 defer。

  1. func b() {
  2. for i := 0; i < 4; i++ {
  3. defer fmt.Print(i)
  4. }
  5. }

在循环中,依次定义了四个 defer 代码块。结合规则一,我们可以明确得知每个 defer 代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了 3210.

设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,跟据 B 资源申请 C 资源,即申请顺序是: A—>B—>C,释放时往往又要反向进行。这就是把 deffer 设计成 FIFO 的原因。

每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。

规则三 defer 可以读取有名返回值

先看下面的代码:

  1. func c() (i int) {
  2. defer func() { i++ }()
  3. return 1
  4. }

输出结果是 2. 在开头的时候,我们说过 defer 是在 return 调用之后才执行的。 这里需要明确的是 defer 代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer 的作用域仍然在 c 函数之内。因此 defer 仍然可以读取 c 函数内的变量 (如果无法读取函数内变量,那又如何进行变量清除呢….)。

当执行 return 1 之后,i 的值就是 1. 此时此刻,defer 代码块开始执行,对 i 进行自增操作。 因此输出 2.

掌握了 defer 以上三条使用规则,那么当我们遇到 defer 代码块时,就可以明确得知 defer 的预期结果

函数返回过程

有一个事实必须要了解,关键字_return_不是一个原子操作,实际上_return_只代理汇编指令_ret_,即将跳转程序执行。比如语句return i,实际上分两步进行,即将<font style="color:#333333;">i</font>值存入栈中作为返回值,然后执行跳转,而<font style="color:#333333;">defer</font>的执行时机正是跳转前,所以说<font style="color:#333333;">defer</font>执行时还是有机会操作返回值的。

举个实际的例子进行说明这个过程:

  1. func deferFuncReturn() (result int) { i := 1
  2. defer func() {
  3. result++
  4. }() return i}

该函数的 return 语句可以拆分成下面两行:

  1. result = i
  2. return

而延迟函数的执行正是在 return 之前,即加入 defer 后的执行过程如下:

  1. result = i //将return 后的变量赋给result
  2. result++ //执行defer逻辑
  3. return //真正的return

所以上面函数实际返回 i++ 值。

关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把 return 语句拆开都可以很好的理解,

3.3.1 主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 “1”、”2”、”Hello” 这样的值,这种情况下 defer 语句是无法操作返回值的。

一个返回字面值的函数,如下所示:

  1. func foo() int { var i int
  2. defer func() {
  3. i++
  4. }() return 1}

上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

3.3.2 主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下 defer 语句可以引用到返回值,但不会改变返回值。

一个返回本地变量的函数,如下所示:

  1. func foo() int { var i int
  2. defer func() {
  3. i++
  4. }() return i
  5. }

上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为 “anony”,上面的返回语句可以拆分成以下过程:

  1. anony = i
  2. i++
  3. return

由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响。

3.3.3 主函数拥有具名返回值

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。

一个影响函返回值的例子:

  1. func foo() (ret int) { defer func() {
  2. ret++
  3. }() return 0}

上面的函数拆解出来,如下所示:

  1. ret = 0
  2. ret++
  3. return

函数真正返回前,在 defer 中对返回值做了 + 1 操作,所以函数最终返回 1。

defer 内部实现原理

本节我们尝试了解一些 defer 的实现机制。

4.1 defer 数据结构

源码包src/src/runtime/runtime2.go:_defer定义了 defer 的数据结构:

  1. type _defer struct {
  2. sp uintptr //函数栈指针
  3. pc uintptr //程序计数器
  4. fn *funcval //函数地址
  5. link *_defer //指向自身结构的指针,用于链接多个defer}

我们知道 defer 后面一定要接一个函数的,所以 defer 的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。

与函数不同的一点是它含有一个指针,可用于指向另一个 defer,每个 goroutine 数据结构中实际上也有一个 defer 指针,该指针指向一个 defer 的单链表,每次声明一个 defer 时就将 defer 插入到单链表表头,每次执行 defer 时就从单链表表头取出一个 defer 执行。

下图展示一个 goroutine 定义多个 defer 时的场景: defer调用 - 图1

从上图可以看到,新声明的 defer 总是添加到链表头部。

函数返回前执行 defer 则是从链表首部依次取出执行,不再赘述。

一个 goroutine 可能连续调用多个函数,defer 添加过程跟上述流程一致,进入函数时添加 defer,离开函数时取出 defer,所以即便调用多个函数,也总是能保证 defer 是按 FIFO 方式执行的。

4.2 defer 的创建和执行

源码包src/runtime/panic.go定义了两个方法分别用于创建 defer 和执行 defer。

  • deferproc(): 在声明 defer 处调用,其将 defer 函数存入 goroutine 的链表中;
  • deferreturn():在 return 指令,准确的讲是在 ret 指令前调用,其将 defer 从 goroutine 链表中取出并执行。

可以简单这么理解,在编译在阶段,声明 defer 处插入了函数 deferproc(),在函数 return 前插入了函数 deferreturn()。

defer 最佳实践

在 golang 当中,defer 代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是 return 之后添加一个函数调用。因此,defer 通常用来释放函数内部变量。

defer 类似于java里面的finally

为了更好的学习 defer 的行为,我们首先来看下面一段代码:

  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. dst, err := os.Create(dstName)
  7. if err != nil {
  8. return
  9. }
  10. written, err = io.Copy(dst, src)
  11. dst.Close()
  12. src.Close()
  13. return
  14. }

这段代码可以运行,但存在’安全隐患’。如果调用 dst, err := os.Create(dstName) 失败,则函数会执行 return 退出运行。但之前创建的 src(文件句柄) 没有被释放。 上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。 如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。 而使用 defer 则可以避免这种情况的发生,下面是使用 defer 的代码:

  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. defer src.Close()
  7. dst, err := os.Create(dstName)
  8. if err != nil {
  9. return
  10. }
  11. defer dst.Close()
  12. return io.Copy(dst, src)
  13. }

通过 defer,我们可以在代码中优雅的关闭 / 清理代码中所使用的变量。

总结

  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)—>执行defer(若有)—>执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯

参考资料