golang教程之Panic and Recover
文章目錄
Panic and Recover
什麼是panic?
處理Go中程式異常情況的慣用方法是使用錯誤。對於程式中出現的大多數異常情況,錯誤就足夠了。
但是在某些情況下程式不能簡單地在異常情況下繼續執行。在這種情況下,我們使用panic
來終止程式。當函式遇到panic
時,將停止執行,執行任何延遲函式,然後控制權返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,此時程式打印出緊急訊息,然後是堆疊跟蹤,然後終止。當我們編寫示例程式時,這個概念會更加清晰。
可以使用recover
重新控制panic
程式,我們將在本教程後面討論。
panic和recover可以被認為類似於其他語言中的try-catch-finally語句。
何時應該使用panic
?
一個重要因素是你應該避免panic
和recover
並使用錯誤。只有在程式無法繼續執行的情況下才應使用恐慌和機制。
panic有兩個有效的用例。
-
一個不可恢復的錯誤,程式不能簡單地繼續執行它。
一個例子是無法繫結到所需埠的Web伺服器。在這種情況下,panic是合理的,因為如果埠繫結本身失敗則沒有別的辦法。
-
程式設計師錯誤。
假設我們有一個接受指標作為引數的方法,有人使用nil作為引數呼叫此方法。在這種情況下,我們可能會感到恐慌,因為呼叫帶有nil引數的方法的程式設計師錯誤是期望有效的指標。
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")
}
以上是列印一個人全名的簡單程式。 第7行中的fullName
函式列印一個人的全名。 此函式檢查firstName
和lastName
指標是否為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
。 因此,遇到panic時,程式執行終止,列印傳遞給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
讓我們回想一下panic的作用。 當函式遇到panic時,將停止執行,執行延遲函式,然後控制權返回其呼叫者。 此過程一直持續到當前goroutine的所有函式都返回,此時程式打印出緊急訊息,然後是堆疊跟蹤,然後終止。
在上面的示例中,我們沒有推遲任何函式呼叫。 如果存在延遲函式呼叫,則執行該呼叫,然後控制元件返回其呼叫者。
讓我們稍微修改上面的例子並使用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
當程式panic時,首先執行延遲函式呼叫,然後控制權返回到執行延遲呼叫的呼叫者,依此類推,直到達到頂級呼叫者。
在我們的案例中,延遲宣告在第8行中。 首先執行fullName
函式。 這列印
deferred call in fullName
然後控制權返回到執行延遲呼叫的main
函式,因此打印出來,
deferred call in main
現在控制權已達到頂級功能,因此程式列印緊急訊息,然後是堆疊跟蹤,然後終止。
Recover
recover
是一個內建函式,用於重新控制panic goroutine。
recover
函式的簽名如下,
func recover() interface{}
只有在延遲函式內部呼叫時,recover才有用。 執行呼叫以在延遲函式內恢復可通過恢復正常執行來停止panic序列,並檢索傳遞給恐慌呼叫的錯誤值。 如果在延遲函式之外呼叫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")
}
第7行中的recoverName()
函式呼叫recover()
返回傳遞給panic呼叫的值。 這裡我們只是列印recover行返回的值。在第14行中延遲recoverName()
在fullName
函式內。
當fullName
發生混亂時,將呼叫延遲函式recoverName()
,它使用recover()
來停止panic序列。
這個程式將列印,
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
當程式panic時,將呼叫延遲的recoverName
函式,然後呼叫recover()
來重新控制恐慌goroutine。 在第8行中呼叫recover()
。 從panic中返回引數,因此打印出來,
recovered from runtime error: last name cannot be nil
在執行recover()
之後,panic停止並且控制返回到呼叫者,在這種情況下,主函式和程式在panic之後繼續從主右邊的第29行正常執行。 它列印通常從main返回,然後在main中延遲呼叫
panic,recover和Goroutines
Recover 僅在從同一goroutine呼叫時才起作用。 從不同的goroutine發生的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中恢復。 函式b()
是單獨的goroutine。並且下一行中的Sleep
只是為了確保程式在b()
執行完畢之前不會終止。
您認為該計劃的產出是什麼? panic會被恢復嗎? 答案是不。 panic將無法恢復。 這是因為recovery
函式存在於不同的gouroutine中,並且panic發生在函式b()
中的不同goroutine中。 因此無法恢復。
執行此程式將輸出,
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就會被恢復。
go b()
修改為
b()
由於panic發生在同一個goroutine,現在將恢復。 如果程式執行上面的更改,它將輸出,
Inside A
Inside B
recovered: oh! B panicked
normally returned from main
執行時Panics
Panics也可能由執行時錯誤引起,例如陣列越界訪問。 這相當於使用由介面型別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
介面滿足內建介面型別錯誤。
讓我們寫一個創造執行時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恢復後通常從main返回。