你真的了解 defer 吗?

Posted 魏小言

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的了解 defer 吗?相关的知识,希望对你有一定的参考价值。

目录

[Go]你真的了解 defer 吗?

为什么需要 defer

defer 语法及语义

defer 使用要点

defer 函数延迟调用

defer 函数参数即时求值

反序调用


[Go]你真的了解 defer 吗?

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

  • 为什么需要 defer;

  • defer 语法及语义;

  • defer 使用要点;

  • defer 语句中的函数到底是在 return 语句之后被调用还是 return 语句之前被调用。

为什么需要 defer

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

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() 来释放资源:

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() 函数始终都会被调用。

关注我 code 杂坛,了解更多......

defer 语法及语义


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

defer xxx(arg0, arg1, arg2, ......)

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

defer 使用要点

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

  • 延迟对函数进行调用;

  • 即时对函数的参数进行求值;

  • 根据 defer 顺序反序调用

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

defer 函数延迟调用

func f() 
     defer fmt.Println("defer")
     fmt.Println("begin")
     fmt.Println("end")
     return

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

defer 函数参数即时求值

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 语句处就被准备好。

关注我 code 杂坛,了解更多......

反序调用

func f() 
     defer fmt.Println("defer01")
     fmt.Println("begin")
     defer fmt.Println("defer02")
     fmt.Println("----")
     defer fmt.Println("defer03")
     fmt.Println("end")
     return

这段程序的输出如下:

begin
----
end
defer03
defer02
defer01

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

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

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

package main

import "fmt"

var g = 100

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

    fmt.Printf("f: g = %d\\n", g)

    return g


func main() 
    i := f()
    fmt.Printf("main: i = %d, g = %d\\n", i, g)

输出:

$ ./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 之后才运行。下面稍微修改一下上面的程序:

package main

import "fmt"

var g = 100

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

    fmt.Printf("f: r = %d\\n", r)

    r = 0
    return r


func main() 
    i := f()
    fmt.Printf("main: i = %d, g = %d\\n", i, g)

输出:

$ ./defer 
f: r = 100
main: i = 200, g = 100

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

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

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

  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  %eax,%eax
  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条汇编指令:

  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 会被翻译成如下几条伪指令

保存返回值到栈上
调用defer函数
调整函数栈
retq指令返回

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

总结

  • defer 主要用于简化编程(以及实现 panic/recover ,后面会专门写一篇相关文章来介绍)

  • defer 实现了函数的延迟调用;

  • defer 使用要点:延迟调用,即时求值和反序调用

  • go 语言的 return 会被编译器翻译成多条指令,其中包括保存返回值,调用defer注册的函数以及实现函数返回。

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

关注我 code 杂坛,了解更多......

以上是关于你真的了解 defer 吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android Studio新功能解析,你真的了解Instant Run吗?

你真的了解什么是CQRS吗?

你真的了解http,https吗?万字长文带你深入了解http!

你真的了解http,https吗?万字长文带你深入了解http!

云存储对象存储管理与安全的知识你真的都了解吗?

开发工具 | 你真的会用jupyter吗?