Golang defer的探索

defer最常在函数调用结束后,在返回值之前被调用。本文将全面讲述defer的操作。

本文主要分析了defer在代码里的各种情况,本文先发于掘金论坛上。欢迎关注我的掘金账号:panzd


1.defer执行顺序

defer关键字的插入顺序时从后向前的,而defer关键字执行是从前向后的,所以后来的defer会优先执行。 当goroutine获取到runtime._defer结构体后,将追加在Goroutine_defer链表的最前面。

图1

2.defer关键词按值传递

defer函数调用都是传值的,会立即复制函数中的引用的外部参数。

例题:

图2

这里f(i)拿到的是i的值。

同理:

图3

在这里,前者的defer拿到的是i这个值,而后者defer拿到的是域变量(指针)。

图4

我在increaseB()加入输出,更能明白:

图5

println拿到的也是值。

3.defer等于nil的函数

图6

图7

可以看到在defer函数启动后,因为nil发生了panic,但在此之前函数是可以顺利运行的。run()的注册也是没有问题的。

4.在循环中的defer

通常情况下,我们不在循环体里用defer,除非特殊的要求。

图8

这里出现了不符合我们预期的结果,在这个循环里的defer函数并没有每次循环都发生打印,而是在整个循环结束后,才开始打印。因为defer调用都被压到defer栈里,等待循环函数结束后出栈。要解决的话,一种是不在循环里放defer,另一种如下:

图9

在defer函数外面再加一层函数,这里defer函数就会在这层函数结束后调用。

5.用defer来封装

有时候,我们需要用defer来关闭外部资源,譬如数据库,IO操作等等。

图10

可以看到defer出现了bug,没有出现断开数据库的连接disconnect,connect()被放在了一边没有运行。

解决方案如下:

图11

先让connect()函数运行,然后defer利用它的return操作来关闭数据库。

当然,我们也可以运用一些go的特性(语法糖),从技术上是相同,但是写法不怎么容易理解。

图12

与上面的方法相同,第一个()连接到数据库,相当于defer db.connect(), 而第二个()则用来运行disconnect方法,在函数调用后,它会执行关闭操作。原因是defer调用了db.connect()关闭操作的值。

6.在块中的defer

刚开始你可以期待deferred函数会在一个代码块结束后调用,后来你才发现deferred函数只会在整个函数结束后调用, 因为defer属于函数func而并非是块block。对于for、switch都是如此。

图13

defer函数是最后输出的。

对此,我们可以适用前面在循环里的操作,将其封装。

图14

7.defer与Scope

让我们定义一个函数,它创建一个deferred函数用来释放资源r。 创建了一个reader用来返回Close过程的error消息,如果Close()方法起作用的话,release()会释放资源。

图15

但这里的输出却是"nil",不是我们想的"Close Error"。

原因是,如果block隐式地用新的err变量替代了原本的err变量,而release()只会返回原本err的值。我们只需仍然使用之前的err,用"=“代替掉”:="。就会解决这个问题。

图16

8.Defer在loop的传参

我们创建一个循环,并用defer输出:

图17

发现全部都是3,这是因为defer只看到了i循环结束后最新的值,Goruntime是将i的地址捕获了后直接传给了defer。

解决方法1就是直接把参数传给defer:

图18

Goruntime在循环中创建了不同的i变量,并且将其保存下来,每个defer即可以看到属于它的i变量。

解决方法2就是在循环中用新的i变量隐藏原本的i:

图19

9.Defer在loop的传参

我们在defer函数里用了return语句,但却失效了:

图20

直接返回了nil,而不是error。

图21

我们指定一个新值给release()函数的结果,这样defer就不用直接返回值,而是帮助release()返回值。

10.调用recover()

一般情况下,我们要在defer里面调用recover()。当panic发生的时候,recover()不在defer里面的话,就无法catch掉panic,这时候recover()只会返回nil。

图22

这时候需要将recover()放到defer里面:

图23

11.调用defer的顺序出错

图24

发生了panic:

图25

因为我们没有去检查这个url请求是否正确,当它的地址错误的时候,会生成一个nil值,再调用Body就会发生panic。

正确的方法要将defer放在一个成功的资源分配后,需要在此之前检查返回结果。

图26

12.没有对错误进行检查

我们在defer里面写好了清理资源的逻辑,并不代表着这个资源就会毫无问题释放掉,它可能产生了隐式的错误,但我们没有发现有效的error信息。

图27

f.Close()并没有成功,但返回了error信息,但我们没有意识到。

正确的写法应该要check一下err,并打印出来:

图28

亦或是用一个新的结果来返回defer里的error:

图29

13.defer 释放同一种资源

如果我们用同一个变量来close掉同一种资源两次,会发生一些错误:

图30

它的问题正是之前循环里发生的一样,这样写,所有defer只能使用到最新的值,只会返回同一种结果。

只需要为每个defer单独设置变量。

图31

结束

defer的探索就到此结束,感谢阅读,一起进步。

参考资料:

  1. Github:go-demo

  2. 5 Gotchas of Defer in Go — Part I

  3. 5 More Gotchas of Defer in Go — Part II

  4. 5 More Gotchas of Defer in Go — Part III

  5. Go 语言设计与实现

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus