- 原文地址:Part 32: Panic and Recover
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
什麼是panic?
處理Go中異常情況的慣用方法是使用errors,對於程式中出現的大多數異常情況,errors就足夠了。
但是在某些情況下程式不能在異常情況下繼續正常執行。在這種情況下,我們使用panic來終止程式。函式遇到panic時將會停止執行,如果有defer的話就執行defer延遲函式,然後返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,然後列印出panic資訊,然後是堆疊資訊,然後程式終止。待會兒用一個例子來解釋,這個概念就會更加清晰一些了。
我們可以使用recover函式恢復被panic終止的程式,將在本教程後面討論。
panic和recover有點類似於其他語言中的try-catch-finally語句,但是前者使用的比較少,而且使用時更優雅程式碼也更簡潔。
什麼時候應該用panic?
一般情況下我們應該避免使用panic和recover,儘可能使用errors。只有在程式無法繼續執行的情況下才應該使用panic和recover。
兩個panic典型應用場景
-
不可恢復的錯誤,讓程式不能繼續進行。 比如說Web伺服器無法繫結到指定埠。在這種情況下,panic是合理的,因為如果埠繫結失敗接下來的邏輯繼續也是沒有意義的。
-
coder的人為錯誤 假設我們有一個接受指標作為引數的方法,然而使用了nil作為引數呼叫此方法。在這種情況下,我們可以用panic,因為該方法需要一個有效的指標。
panic示例
panic函式的定義
func panic(interface{})
複製程式碼
當程式終止時,引數會傳遞給panic函式列印出來。看看下面例子的panic是如何使用的。
package main
import (
"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")
}
複製程式碼
上面這段程式碼,fullName函式功能是列印一個人的全名。此函式檢查firstName和lastName指標是否為nil。如果它為nil,則函式呼叫panic並顯示相應的錯誤訊息。程式終止時將列印此錯誤訊息和錯誤堆疊資訊。
執行此程式將列印以下輸出,
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1040c128, 0x0)
/tmp/sandbox135038844/main.go:12 +0x120
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
複製程式碼
我們來分析一下這個輸出,來了解panic是如何工作以及如何列印堆疊跟蹤的。 在第19行,我們將Elon定義給firstName。然後呼叫fullName函式,其中lastName引數為nil。因此,第11行將觸發panic。當觸發panic時,程式執行就終止了,然後列印傳遞給panic的內容,最後列印堆疊跟蹤資訊。因此14行以後的程式碼不會被執行。 該程式首先列印傳遞給panic函式的內容,
panic: runtime error: last name cannot be nil
複製程式碼
然後列印堆疊跟蹤資訊。 該程式在12行觸發panic,因此,
ain.fullName(0x1040c128, 0x0)
/tmp/sandbox135038844/main.go:12 +0x120
複製程式碼
將被首先列印。然後將列印堆疊中的下一個內容,
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
複製程式碼
現在已經返回到了造成panic的頂層main函式,因此列印結束。
defer函式
我們回想一下panic的作用。當函式遇到panic時,將會終止panic後面程式碼的執行,如果函式體包含有defer函式的話會執行完defer函式。然後返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,此時程式列印出panic內容,然後是堆疊跟蹤資訊,然後終止。
在上面的示例中,我們沒有任何defer函式的呼叫。修改下上面的例子,來看看defer函式的例子吧。
package main
import (
"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")
}
複製程式碼
Run in playground 對之前程式碼所做的唯一更改是在fullName函式和main函式中第一行新增了defer函式呼叫。 執行的輸出,
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1042bf90, 0x0)
/tmp/sandbox060731990/main.go:13 +0x280
main.main()
/tmp/sandbox060731990/main.go:22 +0xc0
複製程式碼
當發生panic時,首先執行defer函式,然後到下一個defer呼叫,依此類推,直到達到頂層呼叫者。
在我們的例子中,defer宣告在fullName函式的第一行。首先執行fullName函式。列印
deferred call in fullName
複製程式碼
然後呼叫返回到main函式的defer,
deferred call in main
複製程式碼
現在呼叫已返回到頂層函式,然後程式列印panic內容,然後是堆疊跟蹤資訊,然後終止。
recover函式
recover是一個內建函式,用於goroutine從panic的中斷狀況中恢復。 函式定義如下,
func recover() interface{}
複製程式碼
recover只有在defer函式內部呼叫時才有效。defer函式內通過呼叫recover可以讓panic中斷的程式恢復正常執行,呼叫recover會返回panic的內容。如果在defer函式之外呼叫recover,它將不會停止panic序列。
修改一下,使用recover來讓panic恢復正常執行。
package main
import (
"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")
}
複製程式碼
Run in playground 第7行呼叫了recoverName函式。這裡列印了recover返回的值, 發現recover返回的是panic的內容。
列印如下,
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
複製程式碼
程式在19行觸發panic,defer函式recoverName通過呼叫recover來重新控制該goroutine,
recovered from runtime error: last name cannot be nil
複製程式碼
在執行recover之後,panic停止並且返回到呼叫者,main函式和程式在觸發panic之後將繼續從第29行執行。然後列印,
returned normally from main
deferred call in main
複製程式碼
Panic, Recover 和 Goroutines
recover僅在從同一個goroutine呼叫時才起作用。從不同的goroutine觸發的panic中recover是不可能的。再來一個例子來加深理解。
package main
import (
"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")
}
複製程式碼
Run in playground 在上面的程式中,函式b在23行觸發panic。函式a呼叫defer函式recovery用於從panic中恢復。函式a的17行用另外一個goroutine執行b函式。Sleep的作用只是為了確保程式在b執行完畢之前不會被終止,當然也可以用sync.WaitGroup來解決。
你認為該段程式碼的輸出是什麼?panic會被恢復嗎?答案是不可以。panic將無法被恢復。這是因為recover存在於不同的gouroutine中,並且觸發panic發生在不同goroutine執行的b函式。因此無法恢復。 執行的輸出,
Inside A
Inside B
panic: oh! B panicked
goroutine 5 [running]:
main.b()
/tmp/sandbox388039916/main.go:23 +0x80
created by main.a
/tmp/sandbox388039916/main.go:17 +0xc0
複製程式碼
可以從輸出中看到恢復失敗了。
如果在同一個goroutine中呼叫函式b,那麼panic就會被恢復。
在第17行把, go b()
換成 b()
那麼會輸出,
Inside A
Inside B
recovered: oh! B panicked
normally returned from main
複製程式碼
執行時的panic
panic還可能由執行時的錯誤引起,例如陣列越界訪問。這相當於使用由介面型別runtime.Error定義的引數呼叫內建函式panic。 runtime.Error介面的定義如下,
type 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。
讓我們寫一個人為的例子來建立執行時panic。
package main
import (
"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")
}
複製程式碼
Run in playground 在上面的程式中,第9行我們試圖訪問n [3],這是切片中的無效索引。這個會觸發panic,輸出如下,
panic: runtime error: index out of range
goroutine 1 [running]:
main.a()
/tmp/sandbox780439659/main.go:9 +0x40
main.main()
/tmp/sandbox780439659/main.go:13 +0x20
複製程式碼
您可能想知道是否執行中的panic能夠被恢復。答案是肯定的。讓我們修改上面的程式,讓panic恢復過來。
package main
import (
"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")
}
複製程式碼
Run in playground 執行後輸出,
Recovered runtime error: index out of range
normally returned from main
複製程式碼
顯然可以看到panic被恢復了。
recover後獲取堆疊資訊
我們恢復了panic,但是丟失了這次panic的堆疊呼叫的資訊。 有一種方法可以解決這個,就是使用Debug包中的PrintStack函式列印堆疊跟蹤資訊
package main
import (
"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")
}
複製程式碼
Run in playground 在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 +0xc0
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()
/tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)
/usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()
/tmp/sandbox949178097/main.go:18 +0x80
main.main()
/tmp/sandbox949178097/main.go:23 +0x20
normally returned from main
複製程式碼
從輸出中可以知道,首先是panic被恢復然後列印Recovered runtime error: index out of range
,再然後列印堆疊跟蹤資訊。最後在panic被恢復後列印normally returned from main