panic(運行時恐慌)是一種只會在程序運行時才回拋出來的異常。在panic被拋出之后,如果沒有在程序里添加任何保護措施的話,程序就會在打印出panic的詳情,終止運行。
如果一個panic是無意間引發的,其中的值只能由Go語言運行時系統給定,但是當使用painc函數有意引發一個panic時,卻可以自行指定其包含的值。
舉個栗子
package main func main() { s1 := []int{0, 1, 2, 3, 4} e5 := s1[5] _ = e5 }
運行上面的代碼,會拋出panic
panic: runtime error: index out of range goroutine 1 [running]: //Id為1的goroutine在此panic被引發時正在運行 main.main() /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.go:5 +0x3d //此行代碼在其所屬源碼文件中的行數,以及源碼文件的絕對路徑, +03d是計數偏移量,用處不大。 exit status 2 //以退出狀態碼2結束運行,一般狀態不為0時表示程序非正常退出
從Painc被引發到程序終止運行的大致過程是怎樣的?
某個函數中的某行代碼引發了一個panic后,初始的panic詳情會被建立起來,並且該程序的控制器會立即從此行代碼轉移到調用其所屬函數的那行代碼上(調用棧中的上一級),此行代碼所屬函數的執行隨即終止。緊接着,控制權並不會在此有片刻停留,它又會立即轉移至上一級的調用代碼處,反方向傳播直至最外層函數(go函數,對於主goroutine來說就是main函數)。但是控制器也不會停留在那里,而是被Go語言運行時系統收回。隨后程序奔潰並終止運行,承載程序這次運行的進程也會隨之死亡並消失。與此同時,在這個控制器傳播過程中,panic詳情會積累和完善,並在程序終止之前打印出來。
//main函數調用了caller1函數,caller1函數調用了caller2函數 goroutine 1 [running]: main.caller2() /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:22 +0x91 main.caller1() /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:15 +0x66 main.main() /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:9 +0x66 exit status 2
怎樣讓panic包含一個值,應該讓她包含什么樣的值?
在調用panic函數,把某個值作為參數傳給該函數
由於panic函數的唯一一個參數時空接口類型的,所以從語法上講,它可以接受任何類型的值。但最好傳入errir類型的錯誤值,或者其他可以被有效序列化(可以更易讀地去表示形式轉換)的值。使程序崩潰時,panic包含的那個值字符串表示形式會被打印出來
怎樣施加panic的保護措施,避免程序奔潰?
Go語言的內建函數recover專用於恢復panic。recover函數無需任何參數,並且會返回一個空接口類型的值。如果用法正確,這個值實際上就是即將恢復的panic包含的值,並且如果這個panic是因我們調用panic函數引發的,那么該值同時也會是此次調用panic函數時,傳入的參數值副本。
上面強調用法正確,那什么是不正確的用法?
package main import ( "fmt" "errors" ) func main() { fmt.Println("Enter function main.") // 引發 panic。 panic(errors.New("something wrong")) p := recover() fmt.Printf("panic: %s\n", p) fmt.Println("Exit function main.") }
在上面這個函數中先通過調用panic函數引發一個panic,緊接着想通過調用recover函數恢復這個panic。但這個recover函數不會起到任何作用,因為panic一旦發生,控制權會沿着調用棧反方向傳播,所以在panic函數調用之后的代碼,根本沒有執行的機會。
那如果把recover函數的代碼提前呢?即先調用recover函數,再調用panic函數。這樣也不行,因為在調用recover函數時未發生panic,那么該函數就不會做任何事情,只會返回一個nil。
那怎樣才是正確的做法呢?
這就要用到defer語句。defer語句是被用來延遲執行代碼的。延遲到該語句所在的函數即將執行結束的那一刻,無論結束執行的原因是什么,即使導致它執行結束的原因是一個panic,因此聯用defer語句和recover函數調用,能夠恢復一個已經發生的panic
package main import ( "fmt" "errors" ) func main() { fmt.Println("Enter function main.") defer func(){ fmt.Println("Enter defer function.") if p := recover(); p != nil { fmt.Printf("panic: %s\n", p) } fmt.Println("Exit defer function.") }() // 引發 panic。 panic(errors.New("something wrong")) fmt.Println("Exit function main.") }
盡量把defer語句寫在函數體開始處,因為在引發panic的語句之后的所有語句,都不會有任何執行機會。只有這樣defer函數中的recover函數調用才會攔截,並恢復defer語句所屬的函數,及其調用代碼中發生的所有panic
如果一個函數中有多條defer語句,那么那幾個defer函數調用的執行順序是怎樣的?
在同一個函數中,defer函數調用的執行順序和它們所屬的defer語句的出現順序完全相反。當一個函數即將結束執行時,寫在最下面的defer函數調用會最先執行,其次是寫在它上邊的與它距離最近的defer函數調用,以此類推
package main import "fmt" func main() { defer fmt.Println("first defer") for i := 0; i < 3; i++ { defer fmt.Printf("defer in for [%d]\n", i) } defer fmt.Println("last defer") } //運行結果 last defer defer in for [2] defer in for [1] defer in for [0] first defer
如果for語句中包含一條defer語句,那這條defer語句執行次數,就取決於for語句迭代次數。並且同一條defer語句每被執行一次,其中的defer調用就會產生一次,而且這些函數調用同樣不會被立即執行。在defer執行時,Go語言會把它攜帶的defer函數及其參數值另行存儲到一個先進后出的隊列,相當於一個棧。在需要執行某個函數中defer函數調用時,Go語言會先拿到對應的隊列,然后從該隊列中一個一個取出defer函數及其參數值,並逐個執行調用
