本文主要分析了defer在代码里的各种情况,本文先发于掘金论坛上。欢迎关注我的掘金账号:panzd
1.defer执行顺序
defer关键字的插入顺序时从后向前的,而defer关键字执行是从前向后的,所以后来的defer会优先执行。 当goroutine获取到runtime._defer结构体后,将追加在Goroutine_defer链表的最前面。
2.defer关键词按值传递
defer函数调用都是传值的,会立即复制函数中的引用的外部参数。
例题:
这里f(i)拿到的是i的值。
同理:
在这里,前者的defer拿到的是i这个值,而后者defer拿到的是域变量(指针)。
我在increaseB()加入输出,更能明白:
println拿到的也是值。
3.defer等于nil的函数
可以看到在defer函数启动后,因为nil发生了panic,但在此之前函数是可以顺利运行的。run()的注册也是没有问题的。
4.在循环中的defer
通常情况下,我们不在循环体里用defer,除非特殊的要求。
这里出现了不符合我们预期的结果,在这个循环里的defer函数并没有每次循环都发生打印,而是在整个循环结束后,才开始打印。因为defer调用都被压到defer栈里,等待循环函数结束后出栈。要解决的话,一种是不在循环里放defer,另一种如下:
在defer函数外面再加一层函数,这里defer函数就会在这层函数结束后调用。
5.用defer来封装
有时候,我们需要用defer来关闭外部资源,譬如数据库,IO操作等等。
可以看到defer出现了bug,没有出现断开数据库的连接disconnect,connect()被放在了一边没有运行。
解决方案如下:
先让connect()函数运行,然后defer利用它的return操作来关闭数据库。
当然,我们也可以运用一些go的特性(语法糖),从技术上是相同,但是写法不怎么容易理解。
与上面的方法相同,第一个()连接到数据库,相当于defer db.connect(), 而第二个()则用来运行disconnect方法,在函数调用后,它会执行关闭操作。原因是defer调用了db.connect()关闭操作的值。
6.在块中的defer
刚开始你可以期待deferred函数会在一个代码块结束后调用,后来你才发现deferred函数只会在整个函数结束后调用, 因为defer属于函数func而并非是块block。对于for、switch都是如此。
defer函数是最后输出的。
对此,我们可以适用前面在循环里的操作,将其封装。
7.defer与Scope
让我们定义一个函数,它创建一个deferred函数用来释放资源r。 创建了一个reader用来返回Close过程的error消息,如果Close()方法起作用的话,release()会释放资源。
但这里的输出却是"nil",不是我们想的"Close Error"。
原因是,如果block隐式地用新的err变量替代了原本的err变量,而release()只会返回原本err的值。我们只需仍然使用之前的err,用"=“代替掉”:="。就会解决这个问题。
8.Defer在loop的传参
我们创建一个循环,并用defer输出:
发现全部都是3,这是因为defer只看到了i循环结束后最新的值,Goruntime是将i的地址捕获了后直接传给了defer。
解决方法1就是直接把参数传给defer:
Goruntime在循环中创建了不同的i变量,并且将其保存下来,每个defer即可以看到属于它的i变量。
解决方法2就是在循环中用新的i变量隐藏原本的i:
9.Defer在loop的传参
我们在defer函数里用了return语句,但却失效了:
直接返回了nil,而不是error。
我们指定一个新值给release()函数的结果,这样defer就不用直接返回值,而是帮助release()返回值。
10.调用recover()
一般情况下,我们要在defer里面调用recover()。当panic发生的时候,recover()不在defer里面的话,就无法catch掉panic,这时候recover()只会返回nil。
这时候需要将recover()放到defer里面:
11.调用defer的顺序出错
发生了panic:
因为我们没有去检查这个url请求是否正确,当它的地址错误的时候,会生成一个nil值,再调用Body就会发生panic。
正确的方法要将defer放在一个成功的资源分配后,需要在此之前检查返回结果。
12.没有对错误进行检查
我们在defer里面写好了清理资源的逻辑,并不代表着这个资源就会毫无问题释放掉,它可能产生了隐式的错误,但我们没有发现有效的error信息。
f.Close()并没有成功,但返回了error信息,但我们没有意识到。
正确的写法应该要check一下err,并打印出来:
亦或是用一个新的结果来返回defer里的error:
13.defer 释放同一种资源
如果我们用同一个变量来close掉同一种资源两次,会发生一些错误:
它的问题正是之前循环里发生的一样,这样写,所有defer只能使用到最新的值,只会返回同一种结果。
只需要为每个defer单独设置变量。
结束
defer的探索就到此结束,感谢阅读,一起进步。
参考资料:
-
Github:go-demo
-
5 Gotchas of Defer in Go — Part I
-
5 More Gotchas of Defer in Go — Part II
-
5 More Gotchas of Defer in Go — Part III
-
Go 语言设计与实现