- 原文地址:Part 32: Panic and Recover
- 原文做者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
處理Go中異常狀況的慣用方法是使用errors,對於程序中出現的大多數異常狀況,errors就足夠了。golang
可是在某些狀況下程序不能在異常狀況下繼續正常執行。在這種狀況下,咱們使用panic來終止程序。函數遇到panic時將會中止執行,若是有defer的話就執行defer延遲函數,而後返回其調用者。此過程一直持續到當前goroutine的全部函數都返回,而後打印出panic信息,而後是堆棧信息,而後程序終止。待會兒用一個例子來解釋,這個概念就會更加清晰一些了。數組
咱們可使用recover函數恢復被panic終止的程序,將在本教程後面討論。bash
panic和recover有點相似於其餘語言中的try-catch-finally語句,可是前者使用的比較少,並且使用時更優雅代碼也更簡潔。服務器
通常狀況下咱們應該避免使用panic和recover,儘量使用errors。只有在程序沒法繼續執行的狀況下才應該使用panic和recover。ide
不可恢復的錯誤,讓程序不能繼續進行。 好比說Web服務器沒法綁定到指定端口。在這種狀況下,panic是合理的,由於若是端口綁定失敗接下來的邏輯繼續也是沒有意義的。函數
coder的人爲錯誤 假設咱們有一個接受指針做爲參數的方法,然而使用了nil做爲參數調用此方法。在這種狀況下,咱們能夠用panic,由於該方法須要一個有效的指針。ui
panic函數的定義spa
func panic(interface{}) 複製代碼
當程序終止時,參數會傳遞給panic函數打印出來。看看下面例子的panic是如何使用的。debug
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函數,所以打印結束。
咱們回想一下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是一個內置函數,用於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
複製代碼
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還可能由運行時的錯誤引發,例如數組越界訪問。這至關於使用由接口類型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被恢復了。
咱們恢復了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