1. 前言
Go 語言中兩個經常成對出現的兩個關鍵字 — panic 和 recover。這兩個關鍵字與上一節提到的 defer 有緊密的聯系,它們都是 Go 語言中的內置函數,也提供了互補的功能。
需要說明兩點
-
panic
能夠改變程序的控制流,調用panic
后會立刻停止執行當前函數的剩余代碼,並在當前Goroutine
中遞歸執行調用方的defer
;- 立刻停止執行當前函數的剩余代碼
- 當前goroutine中遞歸執行調用 defer
-
recover
可以中止panic
造成的程序崩潰。它是一個只能在defer
中發揮作用的函數,在其他作用域中調用不會發揮作用;- recover只能與defer結合使用
2. 現象
- panic 只會觸發當前goroutine的defer
- revoce 只有在defer中調用才能生效
- panic 允許在defer中嵌套多磁調用
2.1 跨協程失效
首先要介紹的現象是 panic
只會觸發當前 Goroutine 的延遲函數調用,通過如下所示的代碼了解該現象:
package main
import (
"fmt"
"time"
)
func main() {
// 主線程中的defer函數並不會執行,因為子協程 panic后,主線程中的defer並不會執行
defer println("in main")
go func() {
defer println("in goroutine")
fmt.Println("子協程running")
panic("子協程崩潰")
}()
time.Sleep(1 * time.Second)
}
# 輸出
$ go run main.go
子協程running
in goroutine
panic: 子協程崩潰
goroutine 6 [running]:
main.main.func1()
...
當運行這段代碼時會發現 main 函數中的 defer 語句並沒有執行,執行的只有當前 Goroutine 中的 defer。
2.2 不起作用的recover
初學 Go 語言工程師可能會寫出下面的代碼,在主程序中調用 recover 試圖中止程序的崩潰,但是從運行的結果中也能看出,下面的程序沒有正常退出。
package main
import "fmt"
func main() {
defer fmt.Println("in main")
if err := recover(); err != nil {
fmt.Println(err)
}
panic("unknown err")
}
# 輸出
$ go run main.go
in main
panic: unknown err
goroutine 1 [running]:
main.main()
D:/gopath/src/Go_base/lesson/panic/demo5.go:11 +0x125
仔細分析一下這個過程就能理解這種現象背后的原因,recover 只有在發生 panic 之后調用才會生效。然而在上面的控制流中,recover 是在 panic 之前調用的,並不滿足生效的條件,所以我們需要在 defer 中使用 recover 關鍵字。
正確的寫法應該是這樣:
package main
import "fmt"
func main() {
defer fmt.Println("in main")
defer func() {
if err := recover(); err != nil {
fmt.Println("occur error")
fmt.Println(err)
}
}()
panic("unknown err")
}
2.3 嵌套使用panic
panic
是可以多次嵌套調用的。,如下所示的代碼就展示了如何在 defer 函數中多次調用 panic:
package main
import "fmt"
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
panic("panic once")
}
# 輸出
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
main.main.func1.1()
...
從上述程序輸出的結果,我們可以確定程序多次調用 panic 也不會影響 defer 函數的正常執行,所以使用 defer 進行收尾工作一般來說都是安全的。
3. panic數據結構
panic
關鍵字在源代碼是由數據結構 runtime._panic
表示的。每當調用panic
都會創建一個如下所示的數據結構存儲相關信息:
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
pc uintptr
sp unsafe.Pointer
goexit bool
}
argp
是指向 defer 調用時參數的指針;arg
是調用panic
時傳入的參數;link
指向了更早調用的runtime._panic
結構;recovered
表示當前runtime._panic
是否被recover
恢復;aborted
表示當前的 panic 是否被強行終止;
具體的panic 程序崩潰與恢復崩潰原理在此不做延伸, 可參考panic與recover
4. 小結
簡單總結一下程序崩潰和恢復的過程:
- 編譯器會負責做轉換關鍵字的工作
- 將 panic 和 recover 分別轉換成 runtime.gopanic 和 runtime.gorecover;
- 將 defer 轉換成 runtime.deferproc 函數
- 在調用 defer 的函數末尾調用 runtime.deferreturn 函數;
- 在運行過程中遇到 runtime.gopanic 方法時,會從 Goroutine 的鏈表依次取出 runtime._defer 結構體並執行;
- 如果調用延遲執行函數時遇到了 runtime.gorecover 就會將 _panic.recovered 標記成 true 並返回 panic 的參數;
- 在這次調用結束之后,runtime.gopanic 會從 runtime._defer 結構體中取出程序計數器 pc 和棧指針 sp 並調用 runtime.recovery 函數進行恢復程序;
- runtime.recovery 會根據傳入的 pc 和 sp 跳轉回 runtime.deferproc;
- 編譯器自動生成的代碼會發現 runtime.deferproc 的返回值不為 0,這時會跳回 runtime.deferreturn 並恢復到正常的執行流程;
- 如果沒有遇到 runtime.gorecover 就會依次遍歷所有的 runtime._defer,並在最后調用 runtime.fatalpanic 中止程序、打印 panic 的參數並返回錯誤碼 2;