轉自個人博客 chinazt.cc
在上一節中,我們介紹了defer的使用。 這一節中,我們溫習一下panic和recover的使用規則。
在golang當中不存在tye ... catch 異常處理邏輯。在golang當中使用defer, panic和recover來控制程序執行流程,借此來達到處理異常的目的。
Panic是一個可以停止程序執行流程的內置函數。 假設當前F函數當中某處代碼觸發panic函數,則F函數停止后面代碼的執行,轉而執行F函數內部的defer函數(如果已經聲明了defer函數的話...),然后結束F函數,將當前處理權轉給F的調用函數。
對於F的調用方M來說,F是調用panic函數結束的,而不是執行return結束的。這一點很重要,因為調用panic函數結束是沒有返回值的。
對於F的調用方M來說,F調用結束並且已經退出。所以引出第一個規則:
調用panic后,調用方函數執行從當前調用點退出
我們通過下面的代碼來解釋這個問題:
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
這段函數里面,當i大於3之后,觸發panic函數。 在i小於3之前,一切正常。 引出我們來分析一下執行邏輯。
首先f()里面,沒有調用g()函數之前,沒有異常。因此會輸出"Calling g."。而后開始調用g(),當輸入的參數小於3之前,會一直輸出"Printing in g",(為什么不輸出"Defer in g"請參考上一篇文章 (defer的使用規則))。
當i4時,開始觸發panic函數,輸出Panicking!。需要記住panic之后,調用方直接從調用點退出。 因為g()里面是迭代調用,因此當i4時觸發panic時,本質是g(i+1)這一行觸發的panic函數。 因此g()后續的函數不再繼續執行,因為存在defer函數了,所以連續輸出三個"Defer in g"。
此時g(0)函數執行完畢,f()函數退出。因為f()函數里面存在defer函數,因此會調用defer 輸出"Recoverd in f"。
所以最終的輸出結果將是:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
聊完執行順序的問題后,我們來看為什么會輸出"Recovered in f 4". 這就是規則二:
規則二 通過panic可以設定返回值
panic存在的意義,不僅可以控制異常處理流程,還可以用來返回異常原因。如果panic不給調用方返回異常原因,那么調用方就無從下手處理問題。 因此在調用panic時,一般來說都是返回一個字符串,用來標示失敗原因。例如上面代碼中的"XXX值不得大於3"。
panic的返回值,通過recover函數來獲取。 recover函數也是一個內置函數,專門用來接收panic函數返回值。當panic函數沒有被調用或者沒有返回值時,recover返回Nil.
在使用recover函數時,就需要下面的規則三:
規則三 recover函數只有在defer代碼塊中才會有效果
注意此處的有效果不等於執行。言外之意,recover函數可以在任何地方執行,但只有在defer代碼塊中執行才能真正達到處理異常的目的。
我們假設,將recover函數移除defer代碼塊,如下:
func f() int {
fmt.Println("Calling g.")
m := g(0)
r := recover()
if r != nil {
fmt.Println("Recovered in f", r)
}
fmt.Println("Returned normally from g.", m)
return m
}
因此recover函數在外,因此執行結果如下:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
goroutine 1 [running]:
[stack trace omitted]
可以看到recover函數並未生效,其實是沒有執行,因為根據規則一,當前執行流程會跳轉到defer函數中。因此只有將recover函數定義到defer之中才會真正被執行。
那么問題來了,recover函數應該定義在哪一級的defer中。 golang是逐級查找的,最終會查找到main函數。 如果main函數中的defer還沒有recover函數,golang這會像上面那樣拋出最終的異常信息。
本節內容不多,但個人感覺還都是干貨。 畢竟我們不能保證自己的寫的代碼一定就沒有問題,有問題不可怕,可怕的是代碼不受控制。所以學好defer,panci和recover,有助於寫出健壯的代碼。