上一期我們講了 Go 中的錯誤處理,這一期講講 Go 中的 異常 處理。錯誤和異常是兩個不同的概念,非常容易混淆。錯誤指的是可能出現問題的地方出現了問題;而異常指的是不應該出現問題的地方出現了問題。
panic
在有些情況,當程式發生異常時,無法繼續運作。在這種情況下,我們會使用
panic
來終止程式。當函數發生
panic
時,它會終止運作,在執行完所有的延遲函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出
panic
資訊,接着列印出堆棧跟蹤,最後程式終止。
我們應該盡可能地使用錯誤,而不是使用
panic
和
recover
。隻有當程式不能繼續運作的時候,才應該使用
panic
和
recover
機制。
panic
有兩個合理的用例:
- 發生了一個不能恢複的錯誤,此時程式不能繼續運作。一個例子就是 web 伺服器無法綁定所要求的端口。在這種情況下,就應該使用
,因為如果不能綁定端口,啥也做不了。panic
- 發生了一個程式設計上的錯誤。假如我們有一個接收指針參數的方法,而其他人使用
作為參數調用了它。在這種情況下,我們可以使用 nil
,因為這是一個程式設計錯誤:用 panic
參數調用了一個隻能接收合法指針的方法。nil
觸發 panic
下面是内建函數
panic
的簽名:
func panic(v interface{})
當程式終止時,會列印傳入
panic
的參數。
package main
func main() {
panic("runtime error")
}
運作上面的程式,會列印出傳入
panic
函數的資訊,并列印出堆棧跟蹤:
panic: runtime error
goroutine 1 [running]:
main.main()
(...)/main.go:4 +0x27
發生 panic 時的 defer
上面已經提到了,當函數發生
panic
時,它會終止運作,在執行完所有的延遲函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出
panic
資訊,接着列印出堆棧跟蹤,最後程式終止。下面通過一個簡單的例子看看是不是這樣:
package main
import "fmt"
func myTest() {
defer fmt.Println("defer in myTest")
panic("runtime error")
}
func main() {
defer fmt.Println("defer in main")
myTest()
}
運作該程式後輸出如下:
defer in myTest
defer in main
panic: runtime error
goroutine 1 [running]:
main.myTest()
(...)/main.go:7 +0x73
main.main()
(...)/main.go:11 +0x70
recover
recover
是一個内建函數,用于重新獲得
panic
協程的控制。下面是内建函數
recover
的簽名:
func recover() interface{}
recover
必須在
defer
函數中才能生效,在其他作用域下,它是不工作的。在延遲函數内調用
recover
,可以取到
panic
的錯誤資訊,并且停止
panic
續發事件,程式運作恢複正常。下面是網上找的一個例子:
package main
import "fmt"
func myTest(x int) {
defer func() {
// recover() 可以将捕獲到的 panic 資訊列印
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var array [10]int
array[x] = 1
}
func main() {
// 故意制造數組越界 觸發 panic
myTest(20)
// 如果能執行到這句 說明 panic 被捕獲了
// 後續的程式能繼續運作
fmt.Println("everything is ok")
}
雖然該程式觸發了
panic
,但由于我們使用了
recover()
捕獲了
panic
異常,并輸出
panic
資訊,即使
panic
會導緻整個程式退出,但在退出前,有
defer
延遲函數,還是得執行完
defer
。然後程式還會繼續執行下去:
runtime error: index out of range [20] with length 10
everything is ok
這裡要注意一點,隻有在相同的協程中調用
recover
才管用,
recover
不能恢複一個不同協程的
panic
。
package main
import (
"fmt"
"time"
)
func main() {
// 這個 defer 并不會執行
defer fmt.Println("in main")
go func() {
defer println("in goroutine")
panic("")
}()
time.Sleep(time.Second)
}
運作後輸出如下:
in goroutine
panic:
goroutine 19 [running]:
main.main.func1()
(...)/main.go:13 +0x7b
created by main.main
(...)/main.go:11 +0x79
參考文獻:
[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程式設計語言, Translated by 李道兵, 高博, 龐向才, 金鑫鑫 and 林齊斌, 機械工業出版社, 2017.