天天看點

Go 語言系列30:異常處理

上一期我們講了 Go 中的錯誤處理,這一期講講 Go 中的 異常 處理。錯誤和異常是兩個不同的概念,非常容易混淆。錯誤指的是可能出現問題的地方出現了問題;而異常指的是不應該出現問題的地方出現了問題。

Go 語言系列30:異常處理

panic

Go 語言系列30:異常處理

在有些情況,當程式發生異常時,無法繼續運作。在這種情況下,我們會使用 ​

​panic​

​​ 來終止程式。當函數發生 ​

​panic​

​​ 時,它會終止運作,在執行完所有的延遲函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出 ​

​panic​

​ 資訊,接着列印出堆棧跟蹤,最後程式終止。

我們應該盡可能地使用錯誤,而不是使用 ​

​panic​

​​ 和 ​

​recover​

​​ 。隻有當程式不能繼續運作的時候,才應該使用 ​

​panic​

​​ 和 ​

​recover​

​ 機制。

​panic​

​ 有兩個合理的用例:

  • 發生了一個不能恢複的錯誤,此時程式不能繼續運作。一個例子就是 web 伺服器無法綁定所要求的端口。在這種情況下,就應該使用 ​

    ​panic​

    ​ ,因為如果不能綁定端口,啥也做不了。
  • 發生了一個程式設計上的錯誤。假如我們有一個接收指針參數的方法,而其他人使用 ​

    ​nil​

    ​ 作為參數調用了它。在這種情況下,我們可以使用 ​

    ​panic​

    ​ ,因為這是一個程式設計錯誤:用 ​

    ​nil​

    ​ 參數調用了一個隻能接收合法指針的方法。
Go 語言系列30:異常處理

觸發 panic

Go 語言系列30:異常處理

下面是内建函數 ​

​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      
Go 語言系列30:異常處理

發生 panic 時的 defer

Go 語言系列30:異常處理

上面已經提到了,當函數發生 ​

​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      
Go 語言系列30:異常處理

recover

Go 語言系列30:異常處理

​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.