這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。[panic 和 recover](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/panic-recover-golang-2-2.png)歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 32 篇。## 什麼是 panic?在 Go 語言中,程式中一般是使用[錯誤](https://studygolang.com/articles/12724)來處理異常情況。對於程式中出現的大部分異常情況,錯誤就已經夠用了。但在有些情況,當程式發生異常時,無法繼續運行。在這種情況下,我們會使用 `panic` 來終止程式。當[函數](https://studygolang.com/articles/11892)發生 panic 時,它會終止運行,在執行完所有的[延遲](https://studygolang.com/articles/12719)函數後,程式控制返回到該函數的調用方。這樣的過程會一直持續下去,直到當前[協程](https://studygolang.com/articles/12342)的所有函數都返回退出,然後程式會列印出 panic 資訊,接著列印出堆疊追蹤(Stack Trace),最後程式終止。在編寫一個樣本程式後,我們就能很好地理解這個概念了。在本教程裡,我們還會接著討論,當程式發生 panic 時,使用 `recover` 可以重新獲得對該程式的控制。可以認為 `panic` 和 `recover` 與其他語言中的 `try-catch-finally` 語句類似,只不過一般我們很少使用 `panic` 和 `recover`。而當我們使用了 `panic` 和 `recover` 時,也會比 `try-catch-finally` 更加優雅,代碼更加整潔。## 什麼時候應該使用 panic?**需要注意的是,你應該儘可能地使用[錯誤](https://studygolang.com/articles/12724),而不是使用 panic 和 recover。只有當程式不能繼續啟動並執行時候,才應該使用 panic 和 recover 機制**。panic 有兩個合理的用例。1. **發生了一個不能恢複的錯誤,此時程式不能繼續運行**。 一個例子就是 網頁伺服器無法綁定所要求的連接埠。在這種情況下,就應該使用 panic,因為如果不能綁定連接埠,啥也做不了。2. **發生了一個編程上的錯誤**。 假如我們有一個接收指標參數的方法,而其他人使用 `nil` 作為參數調用了它。在這種情況下,我們可以使用 panic,因為這是一個編程錯誤:用 `nil` 參數調用了一個只能接收合法指標的方法。## panic 樣本內建函數 `panic` 的簽名如下所示:```gofunc panic(interface{}) ```當程式終止時,會列印傳入 `panic` 的參數。我們寫一個樣本,你就會清楚它的用途了。我們現在就開始吧。我們會寫一個例子,來展示 `panic` 如何工作。```gopackage mainimport ( "fmt")func fullName(firstName *string, lastName *string) { if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName")}func main() { firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main")}```[在 playground 上運行](https://play.golang.org/p/xQJYRSCu8S)上面的程式很簡單,會列印一個人的全名。第 7 行的 `fullName` 函數會列印出一個人的全名。該函數在第 8 行和第 11 行分別檢查了 `firstName` 和 `lastName` 的指標是否為 `nil`。如果是 `nil`,`fullName` 函數會調用含有不同的錯誤資訊的 `panic`。當程式終止時,會列印出該錯誤資訊。運行該程式,會有如下輸出:```panic: runtime error: last name cannot be nilgoroutine 1 [running]: main.fullName(0x1040c128, 0x0) /tmp/sandbox135038844/main.go:12 +0x120main.main() /tmp/sandbox135038844/main.go:20 +0x80```我們來分析這個輸出,理解一下 panic 是如何工作的,並且思考當程式發生 panic 時,會怎樣列印堆疊追蹤。在第 19 行,我們將 `Elon` 賦值給了 `firstName`。在第 20 行,我們調用了 `fullName` 函數,其中 `lastName` 等於 `nil`。因此,滿足了第 11 行的條件,程式發生 panic。當出現了 panic 時,程式就會終止運行,列印出傳入 panic 的參數,接著列印出堆疊追蹤。因此,第 14 行和第 15 行的代碼並不會在發生 panic 之後執行。程式首先會列印出傳入 `panic` 函數的資訊:```panic: runtime error: last name cannot be empty ```接著列印出堆疊追蹤。程式在 `fullName` 函數的第 12 行發生 panic,因此,首先會列印出如下所示的輸出。```main.fullName(0x1040c128, 0x0) /tmp/sandbox135038844/main.go:12 +0x120```接著會列印出堆棧的下一項。在本例中,堆疊追蹤中的下一項是第 20 行(因為發生 panic 的 `fullName` 調用就在這一行),因此接下來會列印出:```main.main() /tmp/sandbox135038844/main.go:20 +0x80```現在我們已經到達了導致 panic 的頂層函數,這裡沒有更多的層級,因此結束列印。## 發生 panic 時的 defer我們重新總結一下 panic 做了什麼。**當函數發生 panic 時,它會終止運行,在執行完所有的延遲函數後,程式控制返回到該函數的調用方。這樣的過程會一直持續下去,直到當前協程的所有函數都返回退出,然後程式會列印出 panic 資訊,接著列印出堆疊追蹤,最後程式終止**。在上面的例子中,我們沒有延遲調用任何函數。如果有延遲函數,會先調用它,然後程式控制返回到函數調用方。我們來修改上面的樣本,使用一個延遲語句。```gopackage mainimport ( "fmt")func fullName(firstName *string, lastName *string) { defer fmt.Println("deferred call in fullName") if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName")}func main() { defer fmt.Println("deferred call in main") firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main")}```[在 playground 上運行](https://play.golang.org/p/oUFnu-uTmC)上述代碼中,我們只修改了兩處,分別在第 8 行和第 20 行添加了延遲函數的調用。該函數會列印:```This program prints,deferred call in fullName deferred call in main panic: runtime error: last name cannot be nilgoroutine 1 [running]: main.fullName(0x1042bf90, 0x0) /tmp/sandbox060731990/main.go:13 +0x280main.main() /tmp/sandbox060731990/main.go:22 +0xc0```當程式在第 13 行發生 panic 時,首先執行了延遲函數,接著控制返回到函數調用方,調用方的延遲函數繼續運行,直到到達頂層調用函數。在我們的例子中,首先執行 `fullName` 函數中的 `defer` 語句(第 8 行)。程式列印出:```deferred call in fullName ```接著程式返回到 `main` 函數,執行了 `main` 函數的延遲調用,因此會輸出:```deferred call in main```現在程式控制到達了頂層函數,因此該函數會列印出 panic 資訊,然後是堆疊追蹤,最後終止程式。## recover`recover` 是一個內建函數,用於重新獲得 panic 協程的控制。`recover` 函數的標籤如下所示:```gofunc recover() interface{}```只有在延遲函數的內部,調用 `recover` 才有用。在延遲函數內調用 `recover`,可以取到 `panic` 的錯誤資訊,並且停止 panic 續發事件(Panicking Sequence),程式運行恢複正常。如果在延遲函數的外部調用 `recover`,就不能停止 panic 續發事件。我們來修改一下程式,在發生 panic 之後,使用 `recover` 來恢複正常的運行。```gopackage mainimport ( "fmt")func recoverName() { if r := recover(); r!= nil { fmt.Println("recovered from ", r) }}func fullName(firstName *string, lastName *string) { defer recoverName() if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName")}func main() { defer fmt.Println("deferred call in main") firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main")}```[在 playground 上運行](https://play.golang.org/p/I9pp8N55c1)在第 7 行,`recoverName()` 函數調用了 `recover()`,返回了調用 `panic` 的傳參。在這裡,我們只是列印出 `recover` 的傳回值(第 8 行)。在 `fullName` 函數內,我們在第 14 行延遲調用了 `recoverNames()`。當 `fullName` 發生 panic 時,會調用延遲函數 `recoverName()`,它使用了 `recover()` 來停止 panic 續發事件。該程式會輸出:```recovered from runtime error: last name cannot be nil returned normally from main deferred call in main ```當程式在第 19 行發生 panic 時,會調用延遲函數 `recoverName`,它反過來會調用 `recover()` 來重新獲得 panic 協程的控制。第 8 行調用了 `recover`,返回了 `panic` 的傳參,因此會列印:```recovered from runtime error: last name cannot be nil ```在執行完 `recover()` 之後,panic 會停止,程式控制返回到調用方(在這裡就是 `main` 函數),程式在發生 panic 之後,從第 29 行開始會繼續正常地運行。程式會列印 `returned normally from main`,之後是 `deferred call in main`。## panic,recover 和 Go 協程只有在相同的 [Go 協程](https://studygolang.com/articles/12342)中調用 recover 才管用。`recover` 不能恢複一個不同協程的 panic。我們用一個例子來理解這一點。```gopackage mainimport ( "fmt" "time")func recovery() { if r := recover(); r != nil { fmt.Println("recovered:", r) }}func a() { defer recovery() fmt.Println("Inside A") go b() time.Sleep(1 * time.Second)}func b() { fmt.Println("Inside B") panic("oh! B panicked")}func main() { a() fmt.Println("normally returned from main")}```[在 playground 上運行](https://play.golang.org/p/pEVzTLz36Y)在上面的程式中,函數 `b()` 在第 23 行發生 panic。函數 `a()` 調用了一個延遲函數 `recovery()`,用於恢複 panic。在第 17 行,函數 `b()` 作為一個不同的協程來調用。下一行的 `Sleep` 只是保證 `a()` 在 `b()` 運行結束之後才退出。你認為程式會輸出什嗎?panic 能夠恢複嗎?答案是否定的,panic 並不會恢複。因為調用 `recovery` 的協程和 `b()` 中發生 panic 的協程並不相同,因此不可能恢複 panic。運行該程式會輸出:```Inside A Inside B panic: oh! B panickedgoroutine 5 [running]: main.b() /tmp/sandbox388039916/main.go:23 +0x80created by main.a /tmp/sandbox388039916/main.go:17 +0xc0```從輸出可以看出,panic 沒有恢複。如果函數 `b()` 在相同的協程裡調用,panic 就可以恢複。如果程式的第 17 行由 `go b()` 修改為 `b()`,就可以恢複 panic 了,因為 panic 發生在與 recover 相同的協程裡。如果運行這個修改後的程式,會輸出:```Inside A Inside B recovered: oh! B panicked normally returned from main```## 運行時 panic執行階段錯誤(如數組越界)也會導致 panic。這等價於調用了內建函數 `panic`,其參數由介面類型 [runtime.Error](https://golang.org/src/runtime/error.go?s=267:503#L1) 給出。`runtime.Error` 介面的定義如下:```gotype Error interface { error // RuntimeError is a no-op function but // serves to distinguish types that are run time // errors from ordinary errors: a type is a // run time error if it has a RuntimeError method. RuntimeError()}```而 `runtime.Error` 介面滿足內建介面類型 [`error`](https://golangbot.com/error-handling/#errortyperepresentation)。我們來編寫一個樣本,建立一個運行時 panic。```gopackage mainimport ( "fmt")func a() { n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a")}func main() { a() fmt.Println("normally returned from main")}```[在 playground 上運行](https://play.golang.org/p/CBsK2xXzGg)在上面的程式中,第 9 行我們試圖訪問 `n[3]`,這是一個對[切片](https://studygolang.com/articles/12121)的錯誤引用。該程式會發生 panic,輸出如下:```panic: runtime error: index out of rangegoroutine 1 [running]: main.a() /tmp/sandbox780439659/main.go:9 +0x40main.main() /tmp/sandbox780439659/main.go:13 +0x20```你也許想知道,是否可以恢複一個運行時 panic?當然可以!我們來修改一下上面的代碼,恢複這個 panic。```gopackage mainimport ( "fmt")func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) }}func a() { defer r() n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a")}func main() { a() fmt.Println("normally returned from main")}```[在 playground 上運行](https://play.golang.org/p/qusvZe5rft)運行上面程式會輸出:```Recovered runtime error: index out of range normally returned from main ```從輸出可以知道,我們已經恢複了這個 panic。## 恢複後獲得堆疊追蹤當我們恢複 panic 時,我們就釋放了它的堆疊追蹤。實際上,在上述程式裡,恢複 panic 之後,我們就失去了堆疊追蹤。有辦法可以列印出堆疊追蹤,就是使用 [`Debug`](https://golang.org/pkg/runtime/debug/) 包中的 [`PrintStack`](https://golang.org/pkg/runtime/debug/#PrintStack) 函數。```gopackage mainimport ( "fmt" "runtime/debug")func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) debug.PrintStack() }}func a() { defer r() n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a")}func main() { a() fmt.Println("normally returned from main")}```[在 playground 上運行](https://play.golang.org/p/D-QlDmumHV)在上面的程式中,我們在第 11 行使用了 `debug.PrintStack()` 列印堆疊追蹤。該程式會輸出:```Recovered runtime error: index out of range goroutine 1 [running]: runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c) /usr/local/go/src/runtime/debug/stack.go:24 +0xc0runtime/debug.PrintStack() /usr/local/go/src/runtime/debug/stack.go:16 +0x20main.r() /tmp/sandbox949178097/main.go:11 +0xe0panic(0xf0a80, 0x17cd50) /usr/local/go/src/runtime/panic.go:491 +0x2c0main.a() /tmp/sandbox949178097/main.go:18 +0x80main.main() /tmp/sandbox949178097/main.go:23 +0x20normally returned from main ```從輸出我們可以看出,首先已經恢複了 panic,列印出 `Recovered runtime error: index out of range`。此外,我們也列印出了堆疊追蹤。在恢複了 panic 之後,還列印出 `normally returned from main`。本教程到此結束。簡單概括一下本教程討論的內容:- 什麼是 panic?- 什麼時候應該使用 panic?- panic 樣本- 發生 panic 時的 defer- recover- panic,recover 和 Go 協程- 運行時 panic- 恢複後獲得堆疊追蹤祝你愉快。**上一教程 - [自訂錯誤](https://studygolang.com/articles/12784)****下一教程 - [頭等函數](https://studygolang.com/articles/12789)**
via: https://golangbot.com/panic-and-recover/
作者:Nick Coghlan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
2015 次點擊