翻譯自:https://golangbot.com/panic-and-recover/
什麼是 panic?
在 Go 程式中,一般是使用錯誤來處理異常情況。對于程式中出現的大部分異常情況,錯誤就已經夠用了。
但在有些情況,當程式發生異常時,無法繼續運作。在這種情況下,我們會使用
panic
來終止程式。當函數發生
panic
時,它會終止運作,在執行完所有的
defer
函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出
panic
資訊,接着列印出
堆棧跟蹤
(
Stack Trace
),最後程式終止。當我們編寫一個示例程式後,我們就能很好地了解這個概念了。
我們将在本教程後面讨論,當程式發生
panic
時,使用
recover
可以重新獲得對該程式的控制。
panic
和
recover
可以認為類似于其他語言中的
try-catch-finally
語句,雖然它使用起來更優雅,代碼更簡潔,隻不過我們很少使用它。
什麼時候應該使用 panic?
需要注意的是,你應該盡可能地使用錯誤處理,而不是使用
panic
和
recover
。隻有當程式不能繼續運作的時候,才應該使用
panic
和
recover
機制。
panic
有兩個合理的用例:
- 發生了一個不能恢複的錯誤,此時程式不能繼續運作。 一個例子就是 web 伺服器無法綁定所要求的端口。在這種情況下,就應該使用 panic,因為如果端口綁定失敗,則什麼也做不了。
- 程式員的錯誤。 假如我們有一個接收指針參數的方法,而其他人使用 nil 作為參數調用了它。在這種情況下,我們可以使用 panic,因為這是一個程式設計錯誤:用 nil 參數調用了一個隻能接收有效指針的方法。
panic 示例
内置函數
panic
的簽名如下所示:
當程式終止時,會列印傳入
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")
}
上面的程式很簡單,會列印一個人的全名。第
7
行的
fullName
函數會列印出一個人的全名。該函數在第
8
行和第
11
行分别檢查了
firstName
和
lastName
的指針是否為
nil
。如果是
nil
,
fullName
函數會調用含有不同的錯誤資訊的
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
是如何工作的,并且當程式發生
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
行,因為
fullName
函數調用導緻發生了
panic
,是以接下來會列印出:
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
現在我們已經到達了導緻
panic
的頂層函數,這裡沒有更多的層級,是以結束列印。
發生 panic 時的 defer
讓我們回想一下
panic
的作用。當函數遇到
panic
時,将停止執行,執行所有
defer
函數,然後程式流程傳回到調用方。此過程一直持續到目前協程的所有函數都傳回,此時程式列印出
panic
消息,然後是堆棧跟蹤,最終程式終止。
在上面的示例中,我們沒有延遲調用任何函數。如果存在延遲函數調用,則執行該調用,然後程式流程傳回到調用方。
讓我們稍微修改上面的例子并使用
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")
}
上述代碼中,我們隻修改了兩處,分别在第
8
行和第
20
行添加了延遲函數的調用。
運作程式将會列印:
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
當程式在第
13
行發生
panic
時,首先執行了延遲函數,接着程式控制傳回到函數調用方,調用方的延遲函數繼續運作,依次類推,直到到達頂層調用函數。
在我們的例子中,首先執行
fullName
函數中的
defer
語句(
第 8 行
)。程式列印出:
deferred call in fullName
接着程式傳回到
main
函數,執行了
main
函數的延遲調用,是以會輸出:
deferred call in main
現在程式控制到達了頂層函數,是以該函數會列印出
panic
資訊,然後是堆棧跟蹤,最後終止程式。
recover
recover
是一個内置函數,用于重新獲得
panic
協程的控制。
recover
函數的簽名如下所示:
隻有在延遲函數的内部,調用
recover
才有用。在延遲函數内調用
recover
,可以取到
panic
的錯誤資訊,并且阻止
panic
後續事件發生,程式運作恢複正常。如果在延遲函數的外部調用
recover
,就不能阻止
panic
後續事件。
我們來修改一下程式,在發生
panic
之後,使用
recover
來恢複正常的運作。
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")
}
在第
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
的傳參,是以會列印:
在執行完
recover()
之後,
panic
會停止,程式控制傳回到調用方(
在這裡就是 main 函數
),程式在發生
panic
之後,從第
29
行開始會繼續正常地運作。程式會列印
returned normally from main
,之後是
deferred call in main
。
panic,recover 和 協程
隻有在同一個協程中調用
recover
才管用。
recover
不能恢複一個不同協程的
panic
。我們用一個例子來了解這一點。
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")
}
在上面的程式中,函數
b()
在第
23
行發生
panic
。函數
a()
調用了一個延遲函數
recovery()
,用于恢複
panic
。在第
17
行,建立了一個新的協程。下一行的
Sleep
是要保證
a()
函數 在
b()
運作結束之後才退出。
你認為程式會輸出什麼?
panic
能夠恢複嗎?答案是否定的,
panic
并不會恢複。因為調用
recovery
的協程和
b()
中發生
panic
的協程并不相同,是以不可能恢複
panic
。
運作該程式會輸出:
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
從輸出可以看出,
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給出。
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")
}
在上面的程式第
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")
}
運作上面的程式将輸出,
Recovered runtime error: index out of range
normally returned from main
從輸出中你可以看到我們已經從
panic
中恢複過來。
恢複後獲得堆棧跟蹤
當我們恢複
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")
}
在上面的程式中,我們在第
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
。
本教程到此結束。
簡單概括一下本教程讨論的内容:
- 什麼是panic?
- 何時應該使用panic?
- panic的示例
- 發生 panic 時的 defer
- recover
- panic,recover 和 協程
- 運作時 panic
- 恢複後擷取堆棧跟蹤