Go36-21,22-panic函式、recover函式以及defer語句
panic
panic,Go語言的另外一種錯誤處理方式。嚴格來講,它處理的不是錯誤,而是異常,並且是一種在我們意料之外的程式異常。
panic詳情
要分析panic詳情,首先來生成一個panic。比如在一個切片裡,它的長度是5,但是要通過索引5訪問其中的元素,這樣的訪問是不正確的。比如下面這樣:
func main() { l := []int{1, 2, 3, 4, 5} _ = l[5] }
程式在執行時,會在執行到這行程式碼的時候丟擲panic,提示使用者索引越界了。這不僅僅是個提示。當panic被丟擲後,如果沒有在程式裡新增任何保護措施的話,程式(或者說代表它的那個程序)會在打印出pinic的詳細情況之後終止執行。下面是panic的詳情:
panic: runtime error: index out of range goroutine 1 [running]: main.main() H:/Go/src/Go36/article21/example01/main.go:5 +0x3d exit status 2
先看第一行的內容,其中的“runtime error”表示,這是一個runtime程式碼包中丟擲的panic。在這個panic中,包含一個runtime.Error介面型別的值。runtime.Error介面內嵌了error介面並做了一點擴充套件,runtime包中有不少它的實現型別。實際上,在"panic:"右邊的內容,就是這個panic包含的runtime.Error型別值的字串表示形式。
此外,panic詳情中一本還會包含與引發原因有關的goroutine的程式碼執行資訊。這裡的“goroutine 1 [running]:”表示是一個ID為1的goroutine在這個panic被引發的時候正在執行。
再看下一行,“main.main()”表明了這個goroutine包裝的go函式就是命令原始碼檔案裡的main函式。再往下一行,指出了這個原始碼檔案的絕對路徑,以及程式碼在檔案中所處的行。這一行的最後的+0x3d代表的是:此行程式碼相對於其所屬函式的入口程式計數偏移量。不過,一般這個對我們沒什麼用。
最後的“exit status 2”,是這個程式退出的狀態碼。在大多數作業系統中,只要退出狀態碼不是0,就是非正常結束。在Go語言中,因panic導致的程式結束執行的退出狀態碼一般都會是2。
從panic被引發到程式終止執行的過程
先說一個大致的過程:當引發了一個panic。
這時,初始的panic詳情會被建立起來,並且該程式的控制權會立即從此程式碼行轉移至呼叫起所屬函式的那行程式碼上,也就是呼叫棧中的上一級。這樣就意味著,此行程式碼所屬的函式的執行立即終止。
緊接著,控制權並不會停在當前位置,它又會立即轉移至再上一級的呼叫程式碼處。控制權如此一級一級地沿著呼叫棧的方向轉播至頂端,就是我們編寫的最外層的函式那裡。這個最外層的函式就是go函式,對於主goroutine來說就是main函式。但是控制權到這還不會停止轉移,而是被Go言語執行時的系統回收。
隨後,程式崩潰並終止執行,執行這次程式的程序也會隨之死亡並消失。而在這個控制權傳播的過程中,panic詳情會被逐漸地積累和完善,並會在程式終止之前被打印出來。
這裡再補充一下,函式引發panic與函式返回錯誤值的意義是完全不同的
當函式返回一個非nil的錯誤值時,函式的呼叫方有權選擇不處理,並且不處理的後果往往是不致命的。
當一個panic發生時,如果不施加任何保護措施,那麼導致的後果就是程式崩潰,這顯然是致命的。
下面的例子清楚地展示了上面描述的控制權一級一級向上傳播的過程:
package main import "fmt" func main() { fmt.Println("main Start") caller1() fmt.Println("main End") } func caller1() { fmt.Println("caller1 Start") caller2() fmt.Println("caller1 End") } func caller2() { fmt.Println("caller2 Start") l := []int{1, 2, 3, 4, 5} _ = l[5] fmt.Println("caller2 End") }
這裡,panic詳情會在控制權傳播的過程中,被逐漸地積累和完善。並且,控制權會一級一級地沿著呼叫棧的反方向傳播至頂端。因此,針對某個goroutine的程式碼執行資訊中,呼叫棧底端的資訊會先出現,然後是上一級呼叫的資訊。以此類推,最後才是此呼叫棧頂端的資訊。
所以是,main函式呼叫了caller1函式,而caller1函式又呼叫了caller2函式。那麼caller2函式中程式碼執行的資訊會先出現,然後是caller1函式中程式碼的執行的資訊,最後才是main函式的資訊:
PS H:\Go\src\Go36\article21\example02> go run main.go main Start caller1 Start caller2 Start panic: runtime error: index out of range goroutine 1 [running]: main.caller2() H:/Go/src/Go36/article21/example02/main.go:20 +0xa2 main.caller1() H:/Go/src/Go36/article21/example02/main.go:13 +0x77 main.main() H:/Go/src/Go36/article21/example02/main.go:7 +0x77 exit status 2 PS H:\Go\src\Go36\article21\example02>
到這裡,應該已經對panic被引發後的程式終止的過程有一定的瞭解了。深入瞭解這個過程以及正確的解讀panic詳情是一項必備技能。這在除錯Go程式或為Go程式排查錯誤的時候非常有用。
panic函式、recover函式以及defer語句
如果一個panic是我們在無意間引發的,那麼其中的值只能由Go語言執行時系統給定。但是,當我們使用panic函式有意的引發一個panic的時候,就可以自行指定其包含的值。
panic函式
在呼叫一個panic函式時,把某個值作為引數傳遞給函式就是可以了。panic函式只有一個引數,並且型別是空介面,所以從語法上講,它可以接受任何型別的值。一旦程式異常了,就一定會把異常的相關資訊記錄下來,所以就會需用輸出這個引數的字串表示形式。雖然fmt.Sprintf和fmt.Fprintf這類可以格式化並輸出引數的函式也符合要求。不過,在功能上推薦使用自定義的Error方法或者String方法。因此,為部不同的資料型別分別編寫這兩種方法是首選。這樣,在程式崩潰的時候,panic包含的拿著值的字串表示形式就會被打印出來:
package main import ( "fmt" // "errors" ) func caller() { fmt.Println("caller Start") // panic(errors.New("Something Wrong"))// 正例 panic(fmt.Errorf("Something Wrong %s", "2"))// 正例 // panic(fmt.Println)// 反例 } func main() { fmt.Println("main Start") caller() fmt.Println("main End") }
recover函式
可以施加應對panic的保護措施,避免程式崩潰。Go語言的內建函式recover專門用於恢復panic。recover無需任何引數,並且會返回一個空介面型別的值。這個返回值,就是panic傳入的引數的副本。
下面先看一個錯誤的用法:
func main() { fmt.Println("main Start") panic(errors.New("Something Wrong")) p := reover() fmt.Println(p) fmt.Pringln("main End") }
這裡,引發panic之後,想緊接著呼叫recover函式。但是,函式的執行會在panic這行就終止了,這個recover函式的呼叫根本就沒有機會執行。要想正確的呼叫recover函式,需要用到defer語句。下面是修正過的程式碼:
package main import ( "fmt" "errors" ) func main() { fmt.Println("main Start") defer func() { fmt.Println("defer Start") if p := recover(); p != nil { fmt.Printf("Panic: %s\n", p) } fmt.Println("defer End") }() panic(errors.New("Something Wrong")) fmt.Println("main End") }
在這個main函式中,先編寫了一條defer語句,並且再defer函式中呼叫了recover函式。僅當呼叫的結果不為nil時,也就是panic確實已經發生時,才會列印panic的內容。這裡要儘量把defer語句寫在函式體的開始處,因為引發panic語句之後的所有語句,都不會有任何執行的機會。
defer語句
defer語句就是被用來延遲執行程式碼的。延遲到該語句所在的函式即將執行結束的那一刻,無論結束執行的原因是什麼(包括panic)。
與別的go語句型別,一個defer語句有一個defer關鍵字和一個呼叫表示式組成。這裡存在一些限制,有一些呼叫表示式是不能出現在這裡的:針對Go語句內奸函式的呼叫表示式,以及針對unsafe包中的函式的呼叫表示式。在這裡被呼叫函式可以是有名稱的,也可以是內名的。可以這這裡的函式叫做defer函式或者延遲函式。注意,被延遲執行的是defer函式 ,而不是defer語句 。
defer執行順序如果一個函式中有多條defer語句的情況,那麼defer函式的呼叫的執行順序與它們所屬的defer語句的執行順序完全相反。當一個函式即將結束執行時,其中寫在最下邊的defer函式呼叫會最新執行,最上表的defer函式呼叫會最後一個執行。
在defer語句每次執行的時候,Go語言會把它所攜帶的defer函式及其引數值另行儲存到一個佇列中。這個佇列與該defer語句所屬的函式是對應的,並且它是先進後出(FILO)的,想到與一個棧。在需要執行某個函式的defer函式呼叫的時候,Go語言會先拿到對應的佇列,然後從該佇列中一個一個的取出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") }