深入理解defer(上)defer基础

it2022-05-08  6

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

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

反序调用

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

这段程序的输出如下:

begin----enddefer03defer02defer01

可以看出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) }

输出:

$ ./deferf: g =100main: 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 =100main: 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
转载请注明原文地址: https://win8.8miu.com/read-1450132.html

最新回复(0)