深入理解defer(上)defer基础

2019-06-24 15:25:40 浏览数 (1)

深入理解 defer 分上下两篇文章,本文为上篇,主要介绍如下内容:

  • 为什么需要 defer;
  • defer 语法及语义;
  • defer 使用要点;
  • defer 语句中的函数到底是在 return 语句之后被调用还是 return 语句之前被调用。

为什么需要 defer

先来看一段没有使用 defer 的代码:

代码语言:javascript复制
func f() {
     r := getResource()  //0,获取资源
     ......
     if ... {
          r.release()  //1,释放资源
          return
      }
     ......
     if ... {
          r.release()  //2,释放资源
          return
      }
     ......
     if ... {
          r.release()  //3,释放资源
          return
      }
     ......
     r.release()  //4,释放资源
     return
}

f() 函数首先通过调用 getResource() 获取了某种资源(比如打开文件,加锁等),然后进行了一些我们不太关心的操作,但这些操作可能会导致 f() 函数提前返回,为了避免资源泄露,所以每个 return 之前都调用了 r.release() 函数对资源进行释放。这段代码看起来并不糟糕,但有两个小问题:代码臃肿可维护性比较差。臃肿倒是其次,主要问题在于代码的可维护性差,因为随着开发和维护的进行,修改代码在所难免,一旦对 f() 函数进行修改添加某个提前返回的分支,就很有可能在提前 return 时忘记调用 r.release() 释放资源,从而导致资源泄漏。

那么我们如何改善上述两个问题呢?一个不错的方案就是通过 defer 调用 r.release() 来释放资源:

代码语言:javascript复制
func f() {
      r := getResource()  //0,获取资源
      defer r.release()  //1,注册延迟调用函数,f()函数返回时才会调用r.release函数释放资源
      ......
      if ... {
           return
       }
      ......
      if ... {
           return
       }
      ......
      if ... {
           return
       }
      ......
      return
}

可以看到通过使用 defer 调用 r.release(),我们不需要在每个 return 之前都去手动调用 r.release() 函数,代码确实精简了一点,重要的是不管以后加多少提前 return 的代码,都不会出现资源泄露的问题,因为不管在什么地方 return ,r.release() 函数始终都会被调用。

defer 语法及语义

defer语法很简单,直接在普通写法的函数调用之前加 defer 关键字即可:

代码语言:javascript复制
defer xxx(arg0, arg1, arg2, ......)

defer 表示对紧跟其后的 xxx() 函数延迟到 defer 语句所在的当前函数返回时再进行调用。比如前文代码中注释 1 处的 defer r.release() 表示等 f() 函数返回时再调用 r.release() 。下文我们称 defer 语句中的函数叫 defer函数。

defer 使用要点

对 defer 的使用需要注意如下几个要点:

  • 延迟对函数进行调用;
  • 即时对函数的参数进行求值;
  • 根据 defer 顺序反序调用

下面我们用例子来简单的看一下这几个要点。

defer 函数延迟调用

代码语言:javascript复制
func f() {
      defer fmt.Println("defer")
      fmt.Println("begin")
      fmt.Println("end")
      return
}

这段代码首先会输出 begin 字符串,然后是 end ,最后才输出 defer 字符串。

defer 函数参数即时求值

代码语言:javascript复制
func g(i int) {
    fmt.Println("g i:", i)
}
func f() {
    i := 100
    defer g(i)  //1
    fmt.Println("begin i:", i)
    i = 200
    fmt.Println("end i:", i)
    return
}

这段代码首先输出 begin i: 100,然后输出 end i: 200,最后输出 g i: 100 ,可以看到 g() 函数虽然在f函数返回时才被调用,但传递给 g() 函数的参数还是100,因为代码 1 处的 defer g(i) 这条语句执行时 i 的值是100。也就是说 defer 函数会被延迟调用,但传递给 defer 函数的参数会在 defer 语句处就被准备好。

反序调用

代码语言:javascript复制
func f() {
      defer fmt.Println("defer01")
      fmt.Println("begin")
      defer fmt.Println("defer02")
      fmt.Println("----")
      defer fmt.Println("defer03")
      fmt.Println("end")
      return
}

这段程序的输出如下:

代码语言:javascript复制
begin
----
end
defer03
defer02
defer01

可以看出f函数返回时,第一个 defer 函数最后被执行,而最后一个 defer 函数却第一个被执行。

defer 函数的执行与 return 语句之间的关系

到目前为止,defer 看起来都还比较好理解。下面我们开始把问题复杂化

代码语言:javascript复制
package main

import "fmt"

var g = 100

func f() (r int) {
    defer func() {
        g = 200
    }()

    fmt.Printf("f: g = %dn", g)

    return g
}

func main() {
    i := f()
    fmt.Printf("main: i = %d, g = %dn", i, g)
}

输出:

代码语言:javascript复制
$ ./defer
f: g = 100
main: i = 100, g = 200

这个输出还是比较容易理解,f() 函数在执行 return g 之前 g 的值还是100,所以 main() 函数获得的 f() 函数的返回值是100,因为 g 已经被 defer 函数修改成了200,所以在 main 中输出的 g 的值为200,看起来 defer 函数在 return g 之后才运行。下面稍微修改一下上面的程序:

代码语言:javascript复制
package main

import "fmt"

var g = 100

func f() (r int) {
    r = g
    defer func() {
        r = 200
    }()

    fmt.Printf("f: r = %dn", r)

     r = 0
    return r
}

func main() {
    i := f()
    fmt.Printf("main: i = %d, g = %dn", i, g)
}

输出:

代码语言:javascript复制
$ ./defer 
f: r = 100
main: i = 200, g = 100

从这个输出可以看出,defer 函数修改了 f() 函数的返回值,从这里看起来 defer 函数的执行发生在 return r 之前,然而上一个例子我们得出的结论是 defer 函数在 return 语句之后才被调用执行,这两个结论很矛盾,到底是怎么回事呢?

仅仅从go语言的角度来说确实不太好理解,我们需要深入到汇编来分析一下。

老套路,使用 gdb 反汇编一下 f() 函数:

代码语言:javascript复制
  0x0000000000488a30 < 0>: mov  %fs:0xfffffffffffffff8,%rcx
   0x0000000000488a39 < 9>: cmp  0x10(%rcx),%rsp
   0x0000000000488a3d < 13>: jbe  0x488b33 <main.f 259>
   0x0000000000488a43 < 19>: sub  $0x68,%rsp
   0x0000000000488a47 < 23>: mov  %rbp,0x60(%rsp)
   0x0000000000488a4c < 28>: lea   0x60(%rsp),%rbp
   0x0000000000488a51 < 33>: movq  $0x0,0x70(%rsp) # 初始化返回值r为0
   0x0000000000488a5a < 42>: mov  0xbd66f(%rip),%rax        # 0x5460d0 <main.g>
   0x0000000000488a61 < 49>: mov  %rax,0x70(%rsp)  # r = g
   0x0000000000488a66 < 54>: movl   $0x8,(%rsp)
   0x0000000000488a6d < 61>: lea  0x384a4(%rip),%rax        # 0x4c0f18
   0x0000000000488a74 < 68>: mov  %rax,0x8(%rsp)
   0x0000000000488a79 < 73>: lea  0x70(%rsp),%rax
   0x0000000000488a7e < 78>: mov  %rax,0x10(%rsp)
   0x0000000000488a83 < 83>: callq  0x426c00 <runtime.deferproc>
   0x0000000000488a88 < 88>: test  �x,�x
   0x0000000000488a8a < 90>: jne  0x488b23 <main.f 243>
   0x0000000000488a90 < 96>: mov  0x70(%rsp),%rax
   0x0000000000488a95 < 101>: mov  %rax,(%rsp)
   0x0000000000488a99 < 105>: callq  0x408950 <runtime.convT64>
   0x0000000000488a9e < 110>: mov  0x8(%rsp),%rax
   0x0000000000488aa3 < 115>: xorps  %xmm0,%xmm0
   0x0000000000488aa6 < 118>: movups  %xmm0,0x50(%rsp)
   0x0000000000488aab < 123>: lea  0x101ee(%rip),%rcx        # 0x498ca0
   0x0000000000488ab2 < 130>: mov  %rcx,0x50(%rsp)
   0x0000000000488ab7 < 135>: mov   %rax,0x58(%rsp)
   0x0000000000488abc < 140>: nop
   0x0000000000488abd < 141>: mov  0xd0d2c(%rip),%rax # 0x5597f0 <os.Stdout>
   0x0000000000488ac4 < 148>: lea  0x495f5(%rip),%rcx # 0x4d20c0 <go.itab.*os.File,io.Writer>
   0x0000000000488acb < 155>: mov   %rcx,(%rsp)
   0x0000000000488acf < 159>: mov  %rax,0x8(%rsp)
   0x0000000000488ad4 < 164>: lea   0x31ddb(%rip),%rax        # 0x4ba8b6
   0x0000000000488adb < 171>: mov  %rax,0x10(%rsp)
   0x0000000000488ae0 < 176>: movq   $0xa,0x18(%rsp)
   0x0000000000488ae9 < 185>: lea  0x50(%rsp),%rax
   0x0000000000488aee < 190>: mov  %rax,0x20(%rsp)
   0x0000000000488af3 < 195>: movq  $0x1,0x28(%rsp)
   0x0000000000488afc < 204>: movq  $0x1,0x30(%rsp)
   0x0000000000488b05 < 213>: callq  0x480b20 <fmt.Fprintf>
   0x0000000000488b0a < 218>: movq  $0x0,0x70(%rsp) # r = 0
   # ---- 下面5条指令对应着go代码中的 return r
   0x0000000000488b13 < 227>: nop
   0x0000000000488b14 < 228>: callq  0x427490 <runtime.deferreturn>
   0x0000000000488b19 < 233>: mov  0x60(%rsp),%rbp
   0x0000000000488b1e < 238>: add  $0x68,%rsp
   0x0000000000488b22 < 242>: retq   
   # ---------------------------
   0x0000000000488b23 < 243>: nop
   0x0000000000488b24 < 244>: callq  0x427490 <runtime.deferreturn>
   0x0000000000488b29 < 249>: mov  0x60(%rsp),%rbp
   0x0000000000488b2e < 254>: add  $0x68,%rsp
   0x0000000000488b32 < 258>: retq   
   0x0000000000488b33 < 259>: callq  0x44f300 <runtime.morestack_noctxt>
   0x0000000000488b38 < 264>: jmpq  0x488a30 <main.f>

f() 函数本来很简单,但里面使用了闭包和 Printf,所以汇编代码看起来比较复杂,这里我们只挑重点出来说。f() 函数最后 2 条语句被编译器翻译成了如下6条汇编指令:

代码语言:javascript复制
   0x0000000000488b0a < 218>: movq   $0x0,0x70(%rsp) # r = 0
   # ---- 下面5条指令对应着go代码中的 return r
   0x0000000000488b13 < 227>: nop
   0x0000000000488b14 < 228>: callq  0x427490 <runtime.deferreturn>  # deferreturn会调用defer注册的函数
   0x0000000000488b19 < 233>: mov  0x60(%rsp),%rbp   # 调整栈
   0x0000000000488b1e < 238>: add  $0x68,%rsp  # 调整栈
   0x0000000000488b22 < 242>: retq   # 从f()函数返回
   # ---------------------------

这6条指令中的第一条指令对应到的go语句是 r = 0,因为 r = 0 之后的下一行语句是 return r ,所以这条指令相当于把 f() 函数的返回值保存到了栈上,然后第三条指令调用了 runtime.deferreturn 函数,该函数会去调用我们在 f() 函数开始处使用 defer 注册的函数修改 r 的值为200,所以我们在main函数拿到的返回值是200,后面三条指令完成函数调用栈的调整及返回。

从这几条指令可以得出,准确的说,defer 函数的执行既不是在 return 之后也不是在 return 之前,而是一条go语言的 return 语句包含了对 defer 函数的调用,即 return 会被翻译成如下几条伪指令

代码语言:javascript复制
保存返回值到栈上
调用defer函数
调整函数栈
retq指令返回

到此我们已经知道,前面说的矛盾其实并非矛盾,只是从Go语言层面来理解不好理解而已,一旦我们深入到汇编层面,一切都会显得那么自然,正所谓汇编之下了无秘密

总结

  • defer 主要用于简化编程(以及实现 panic/recover ,后面会专门写一篇相关文章来介绍)
  • defer 实现了函数的延迟调用;
  • defer 使用要点:延迟调用,即时求值和反序调用
  • go 语言的 return 会被编译器翻译成多条指令,其中包括保存返回值,调用defer注册的函数以及实现函数返回。

本文我们主要从使用的角度介绍了defer 的基础知识,下一篇文章我们将会深入 runtime.deferproc 和 runtime.deferreturn 这两个函数分析 defer 的实现机制。


最后,如果你觉得本文对你有帮助的话,麻烦帮忙点一下文末右下角的 在看 或转发到朋友圈,非常感谢!

0 人点赞