Golang defer的探索

378 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

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

defer执行顺序

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

例题:

image.png

defer关键词按值传递

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

例题:

image.png

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

同理:

image.png

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

image.png

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

image.png

println拿到的也是值。

defer 等于nil的函数

image.png

image.png

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

在循环中的defer

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

image.png

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

image.png

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

用defer来封装

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

image.png

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

解决方案如下:

image.png

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

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

image.png

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

在块中的defer

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

image.png

defer函数是最后输出的。

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

image.png

defer与Scope

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

image.png

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

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

image.png

Defer在loop的传参

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

image.png

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

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

image.png

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

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

image.png

return value在defer函数失效

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

image.png

直接返回了nil,而不是error

image.png

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

调用recover()

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

image.png

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

image.png

调用defer的顺序出错

image.png

发生了panic:

image.png

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

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

image.png

没有对错误进行检查

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

image.png

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

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

image.png

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

image.png

defer 释放同一种资源

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

image.png

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

image.png

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

结束

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

参考资料: