defer詳解
defer是Golang一大語言特色,它扮演的是類似Java中finally的角色。用于執(zhí)行釋放資源的一些操作。
defer語句定義在函數(shù)內,后面跟一個函數(shù),被defer的函數(shù)會在defer所在的函數(shù)結果前被執(zhí)行。defer能夠保證函數(shù)不管以何種方式結束(return或panic),被defer的函數(shù)一定被執(zhí)行。
釋放資源
defer可以被定義在函數(shù)中的任何地方,這意味著當我們打開一個資源,馬上可以使用defer聲明它的釋放,這樣我們在編寫后面的代碼的時候就不用操心這個資源的釋放了。函數(shù)結束的時候會自動執(zhí)行釋放操作:
func ReadFile() error {
// 打開了一些資源
file, err := os.Open("file")
if err != nil {
return err
}
// 這條語句會自動在函數(shù)結束時執(zhí)行
defer file.Close()
// 繼續(xù)執(zhí)行后面的操作,而不用操心file的釋放了
...
}
這就是defer的一般用法了,當然,defer遠沒有這么簡單,下面我們來扣扣defer的語法細節(jié):
多個defer的執(zhí)行順序
如果一個函數(shù)里面有多個defer,那么會如何執(zhí)行呢?觀察下面的代碼:
package main
import "fmt"
func main() {
deferFunc()
}
func deferFunc() {
defer fmt.Println("exec 1")
defer fmt.Println("exec 2")
defer fmt.Println("exec 3")
}
注意,defer的執(zhí)行順序遵循后進先出的原則,后面的defer語句將會被先執(zhí)行,因此執(zhí)行的順序應該是從下至上的,下面是輸出:
exec 3
exec 2
exec 1
我們只需要知道defer是從下到上執(zhí)行的即可。
defer和return的順序
defer的順序還有更加深層次的話題,那就是defer和return之間的執(zhí)行順序。
觀察下面的代碼:
package main
import "fmt"
func main() {
fmt.Println(deferReturn())
}
func deferReturn() (i int) {
defer func() {
i += 3
}()
i = 3
return
}
執(zhí)行一下,結果輸出"6"。這說明,defer是在函數(shù)return之前執(zhí)行的。而具名返回參數(shù)i的作用域是整個函數(shù),因此理所當然地,defer可以訪問i并對其做修改。
defer和panic
當函數(shù)遇到panic,defer仍然會被執(zhí)行。Go會先執(zhí)行所有的defer鏈表(該函數(shù)的所有defer),當所有defer被執(zhí)行完畢且沒有recover時,才會進行panic。
package main
import "fmt"
func main() {
deferPanic()
}
func deferPanic() {
defer fmt.Println("exec 1")
defer fmt.Println("exec 2")
defer fmt.Println("exec 3")
panic("出了點小小的錯誤")
}
所有的defer都沒有recover,因此會先從下到上執(zhí)行所有的defer,最后進行panic,程序退出:
exec 3
exec 2
exec 1
panic: 出了點小小的錯誤
defer+recover恢復panic
我們可以在defer中進行recover,如果defer中包含recover,則程序將不會再進行panic,觀察下面代碼:
package main
import "fmt"
func main() {
deferPanic()
}
func deferPanic() {
defer fmt.Println("最后執(zhí)行")
defer func() {
if err := recover(); err != nil {
fmt.Println("發(fā)生了錯誤!")
}
}()
fmt.Println("錯誤之前")
panic("出了點小小的錯誤")
fmt.Println("錯誤之后")
}
程序輸出:
錯誤之前
發(fā)生了錯誤!
最后執(zhí)行
和之前的區(qū)別就是程序沒有再panic退出了。
熟悉其它語言諸如C++、Java、Python的應該知道,這實際上就是這些語言的try...catch功能。所以所謂的golang沒有try...catch功能,處理異常都是返回error的說法其實不準確,Go是有類似try...catch的功能的,只不過寫法和其它語言不一樣。上面的代碼到Java中就是這么寫了:
public class TestTryCatch {
public static void main(String[] args) {
try {
System.out.Println("錯誤之前");
throw new Exception();
System.out.Println("錯誤之后");
} catch (Exception e) {
System.out.Println("發(fā)生了錯誤!");
} finally {
System.out.Println("最后執(zhí)行");
}
}
}
程序輸出和上面的是一樣的。
如果是其它函數(shù)的panic,recover可以成功嗎?觀察下面代碼:
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("產生了錯誤!")
}
}()
panicFun()
}
func panicFun() {
panic("我是錯誤")
}
輸出:
產生了錯誤!
可見,main對panicFunc的panic進行了恢復。
而如果panicFunc自己對panic進行了恢復,則main的恢復將不會再執(zhí)行。觀察下面的代碼:
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("main修復錯誤!")
}
}()
panicFunc()
}
func panicFunc() {
defer func() {
if err := recover(); err != nil {
fmt.Println("panicFunc修復錯誤!")
}
}()
panic("我是錯誤")
}
程序輸出:
panicFunc修復錯誤!
main并沒有去修復panic,因為該panic已經被先執(zhí)行的panicFunc給修復掉了。
最后一個問題來,recover可以檢測到其它goroutine的panic嗎?看下面的代碼:
package main
import (
"fmt"
"time"
)
func main() {
defer fmt.Println("main defer")
defer func() {
if err := recover(); err != nil {
fmt.Println("發(fā)生了錯誤!")
}
}()
go func() {
defer fmt.Println("other defer")
panic("一些小問題")
}()
time.Sleep(1*time.Second)
fmt.Println("程序執(zhí)行完畢")
}
程序輸出:
other defer
panic: 一些小問題
注意這里面有3個defer,有兩個是main的defer,其中一個進行了recover;另外一個是另一個goroutine的defer。我們觀察,盡管代碼中的goroutine是由main創(chuàng)建的,但是當其發(fā)生了panic之后,main goroutine的defer并不會被主動執(zhí)行。
但是,上面的例子退出是因為main中有recover的defer根本沒有機會執(zhí)行,那么,如果給它執(zhí)行的機會呢?panic是否會被recover?觀察下面的代碼:
package main
import (
"fmt"
"time"
)
func main() {
defer fmt.Println("main defer")
defer func() {
if err := recover(); err != nil {
fmt.Println("發(fā)生了錯誤!")
}
}()
go func() {
defer fmt.Println("other defer")
time.Sleep(2 * time.Second) // 隔久一點再產生panic
panic("一些小問題")
}()
time.Sleep(1*time.Second)
fmt.Println("程序執(zhí)行完畢")
time.Sleep(4*time.Second)
}
程序輸出:
程序執(zhí)行完畢
other defer
panic: 一些小問題
還是發(fā)生了panic??梢妋ain的defer不進行recover并不是時間的原因。而是main的recover()并不能發(fā)現(xiàn)其它goroutine的panic。
我們可以總結一下:
- defer+recover可以將當前函數(shù)中發(fā)生的panic給恢復
- defer+recover也可以恢復當前函數(shù)的調用的沒有被recover的其它函數(shù)的panic
- defer+recover不可以恢復其它goroutine的panic
defer參數(shù)陷阱
defer有一個非常費解的陷阱,話不多說,先看下面的代碼:
package main
import "fmt"
func main() {
defer addAndPrint(addAndPrint(1, 2), 3) // defer1
defer addAndPrint(addAndPrint(4, 5), 6) // defer2
}
func addAndPrint(a, b int) int {
sum := a + b
fmt.Printf("%d + %d = %d\n", a, b, sum)
return sum
}
很多golang的新手看到這段代碼,會想,按照defer的執(zhí)行順序,應該會先執(zhí)行defer2,再執(zhí)行defer1,因此理所當然地這段代碼應該輸出:
4 + 5 = 9
9 + 6 = 15
1 + 2 = 3
3 + 3 = 6
真實運行結果讓人大跌眼鏡:
1 + 2 = 3
4 + 5 = 9
9 + 6 = 15
3 + 3 = 6
產生這個結果的原因是,在定義defer的時候,go就需要確定defer語句的函數(shù)的參數(shù)。因此go順序執(zhí)行到defer定義的時候,會直接把defer函數(shù)的參數(shù)計算出來。
在順序執(zhí)行到defer1定義時,其第一個參數(shù)是一個函數(shù),因此Go會先執(zhí)行這個函數(shù),所以第一個執(zhí)行的應該是1 + 2 = 3,隨后遇到defer2,參數(shù)也有函數(shù),執(zhí)行該函數(shù),第二個執(zhí)行的就是4 + 5 = 9。隨后函數(shù)結束,再按照defer的逆序執(zhí)行defer2,defer1。這時候它們的參數(shù)已經被確定,不再需要執(zhí)行其他函數(shù)。
也就是說,Go保證在函數(shù)執(zhí)行結束后,該defer處僅有這個函數(shù)執(zhí)行,其參數(shù)在函數(shù)結束前已經全部確定好,不需要再執(zhí)行其他函數(shù)來確定參數(shù)。