理解 defer、panic 和 recover
在 Go 語言中,有很多流程控制的語句,if、else 等等,這些流程控制的關鍵字在其他語言中也存在的但 Go 中還有幾個特殊的流程控制關鍵字,defer、panic 和 recover。
- defer
defer 可以保證一些代碼在函數或者方法返回之前被調用,即使方法沒有正常執行完,發生了 panic,defer 後面的代碼也會執行。這裏需要注意,不是在退出某個作用域之前會被調用,而且函數或者方法。
defer 通常用來回收一些資源,比如關閉文件,關閉數據庫連接以及釋放一些資源(釋放鎖)。在使用 defer 的時候,有一些需要注意的地方。
- 參數預計算
在定義 defer 的時候,引用的外部參數會立刻被拷貝,在 i++ 執行之前就已經確定了,下面的代碼最後打印的值是 0, 而不是 1:
func t1() {
i := 0
defer fmt.Println(i)
i++
}
如果想要最後打印的值是 1,則做如下修改:
func t2() {
i := 0
defer func() {
fmt.Println(i)
}()
i++
}
- 如果定義了多個 defer 語句,最後定義的最先執行
下面的代碼輸出的結果是 4 3 2 1:
func t3() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
}
- defer 可以對方法或者函數的命名返回值進行賦值
在下面的代碼中,正常情況下回返回 1,使用 defer 卻可以對返回值繼續賦值,所以最後的返回值是 2。
- panic
panic 是 Go 的內置函數,可以打斷當前的代碼的正常執行流程,如果一個函數中出現 panic,該函數後續的代碼都會停止執行。但是會執行 F 中的 defer 代碼。然後其他調用 F 函數的地方也會出現 panic,層層向上傳遞,直到棧頂,最後程序會崩潰。
panic 可以顯示調用 panic 函數產生,也會通過一些運行時的錯誤產生,比如數組越界。
在網上有很多文章會說,panic 只會調用當前 goroutine 的 defer 代碼,其實這種說法是不正確的,比如下面的代碼:
func main() {
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer fmt.Println("goroutine2 invoke")
go func() {
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
}
很大概率會輸出下面的結果:
goroutine1 invoke
goroutine2 invoke
goroutine3 invoke
panic: panic
準確的說法是 panic 只會保證當前 goroutine 中的 defer 代碼一定會執行,其他 goroutine 中的 defer 代碼不保證能執行。
- recover
recover 也是 Go 的內置函數,這個函數可以從 panic 中恢復程序的正常執行。recover 需要和 defer 定義在一起。
在正常的流程中,recover 的執行不會產生任何影響。只有在 panic 發生的時候,recevoer 纔會恢復應用,阻止程序崩潰。而 panic 發生的時候只會執行 defer 代碼。所以 recover 只在和 defer 搭配的時候纔會有意義。
recover 和 panic 需要在同一個 goroutine 使用,跨 goroutine 無法恢復應用。
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer fmt.Println("goroutine2 invoke")
go func() {
defer func() {
recover()
}()
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
下面的程序不會出現崩潰,但如果對 recover 的調用不在同一個 goroutine 中,就無法阻止程序的崩潰。
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer func() {
recover()
}()
defer fmt.Println("goroutine2 invoke")
go func() {
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
- 小結
defer、panic、recover 是 Go 提供的流程控制方式,defer 可以用於正常的代碼流程,用於關閉資源等操作,panic 則用來表示程序出現大問題,需要終止,可以自行觸發,也可以被一些運行時的錯誤觸發。但在一些情況下,我們不希望程序因爲 panic 而終止,比如 web 服務,可以通過 recoever 來恢復程序。
本文基於 go1.14
文 / Rayjun
[1] https://blog.golang.org/defer-panic-and-recover
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/P2BzlvbHtS1H_Ewz5JMZmQ