什麼是 panic?
在 Go 語言中,程式中一般是使用錯誤來處理異常情況。對于程式中出現的大部分異常情況,錯誤就已經夠用了。
當程式發生 panic 時,使用
recover
可以重新獲得對該程式的控制。
可以認為
panic
和
recover
與其他語言中的
try-catch-finally
語句類似,隻不過一般我們很少使用
panic
和
recover
。而當我們使用了
panic
和
recover
時,也會比
try-catch-finally
更加優雅,代碼更加整潔。
什麼時候應該使用 panic?
需要注意的是,你應該盡可能地使用錯誤,而不是使用 panic 和 recover。隻有當程式不能繼續運作的時候,才應該使用 panic 和 recover 機制。
panic 有兩個合理的用例。
- 發生了一個不能恢複的錯誤,此時程式不能繼續運作。 一個例子就是 web 伺服器無法綁定所要求的端口。在這種情況下,就應該使用 panic,因為如果不能綁定端口,啥也做不了。
- 發生了一個程式設計上的錯誤。 假如我們有一個接收指針參數的方法,而其他人使用
作為參數調用了它。在這種情況下,我們可以使用 panic,因為這是一個程式設計錯誤:用 nil
參數調用了一個隻能接收合法指針的方法。nil
panic 示例
func panic(interface{})
我們會寫一個例子,來展示
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")
}
在 playground 上運作
運作該程式,會有如下輸出:
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 行(因為發生 panic 的
fullName
調用就在這一行),是以接下來會列印出:
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
現在我們已經到達了導緻 panic 的頂層函數,這裡沒有更多的層級,是以結束列印。
發生 panic 時的 defer
我們重新總結一下 panic 做了什麼。當函數發生 panic 時,它會終止運作,在執行完所有的延遲函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出 panic 資訊,接着列印出堆棧跟蹤,最後程式終止。
在上面的例子中,我們沒有延遲調用任何函數。如果有延遲函數,會先調用它,然後程式控制傳回到函數調用方。
我們來修改上面的示例,使用一個延遲語句。
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")
}
在 playground 上運作
上述代碼中,我們隻修改了兩處,分别在第 8 行和第 20 行添加了延遲函數的調用。
該函數會列印:
This program prints,
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
函數的标簽如下所示:
func recover() interface{}
隻有在延遲函數的内部,調用
recover
才有用。在延遲函數内調用
recover
,可以取到
panic
的錯誤資訊,并且停止 panic 續發事件(Panicking Sequence),程式運作恢複正常。如果在延遲函數的外部調用
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")
}
在 playground 上運作
在第 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 協程中調用 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")
}
在 playground 上運作
在上面的程式中,函數
b()
在第 23 行發生 panic。函數
a()
調用了一個延遲函數
recovery()
,用于恢複 panic。在第 17 行,函數
b()
作為一個不同的協程來調用。下一行的
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")
}
在 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")
}
在 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")
}
在 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?
- 什麼時候應該使用 panic?
- panic 示例
- 發生 panic 時的 defer
- recover
- panic,recover 和 Go 協程
- 運作時 panic
- 恢複後獲得堆棧跟蹤