1. 程式人生 > >Go36-21,22-panic函數、recover函數以及defer語句

Go36-21,22-panic函數、recover函數以及defer語句

添加 機會 runtime 才會 行存儲 文件 不同 [] 而在

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")
}

Go36-21,22-panic函數、recover函數以及defer語句