5.9. Panic異常

Go的類型系統會在編譯時捕獲很多錯誤,但有些錯誤只能在運行時檢查,如數組訪問越界、空指針引用等。這些運行時錯誤會引起panic異常。

一般而言,當panic異常發生時,程序會中斷運行,並立即執行在該goroutine(可以先理解成線程,在第8章會詳細介紹)中被延遲的函數(defer 機制)。隨後,程序崩潰並輸出日誌信息。日誌信息包括panic value和函數調用的堆棧跟蹤信息。panic value通常是某種錯誤信息。對於每個goroutine,日誌信息中都會有與之相對的,發生panic時的函數調用堆棧跟蹤信息。通常,我們不需要再次運行程序去定位問題,日誌信息已經提供了足夠的診斷依據。因此,在我們填寫問題報告時,一般會將panic異常和日誌信息一併記錄。

不是所有的panic異常都來自運行時,直接調用內置的panic函數也會引發panic異常;panic函數接受任何值作為參數。當某些不應該發生的場景發生時,我們就應該調用panic。比如,當程序到達了某條邏輯上不可能到達的路徑:

switch s := suit(drawCard()); s {
case "Spades":                                // ...
case "Hearts":                                // ...
case "Diamonds":                              // ...
case "Clubs":                                 // ...
default:
    panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
}

斷言函數必須滿足的前置條件是明智的做法,但這很容易被濫用。除非你能提供更多的錯誤信息,或者能更快速的發現錯誤,否則不需要使用斷言,編譯器在運行時會幫你檢查代碼。

func Reset(x *Buffer) {
    if x == nil {
        panic("x is nil") // unnecessary!
    }
    x.elements = nil
}

雖然Go的panic機制類似於其他語言的異常,但panic的適用場景有一些不同。由於panic會引起程序的崩潰,因此panic一般用於嚴重錯誤,如程序內部的邏輯不一致。勤奮的程序員認為任何崩潰都表明代碼中存在漏洞,所以對於大部分漏洞,我們應該使用Go提供的錯誤機制,而不是panic,儘量避免程序的崩潰。在健壯的程序中,任何可以預料到的錯誤,如不正確的輸入、錯誤的配置或是失敗的I/O操作都應該被優雅的處理,最好的處理方式,就是使用Go的錯誤機制。

考慮regexp.Compile函數,該函數將正則表達式編譯成有效的可匹配格式。當輸入的正則表達式不合法時,該函數會返回一個錯誤。當調用者明確的知道正確的輸入不會引起函數錯誤時,要求調用者檢查這個錯誤是不必要和累贅的。我們應該假設函數的輸入一直合法,就如前面的斷言一樣:當調用者輸入了不應該出現的輸入時,觸發panic異常。

在程序源碼中,大多數正則表達式是字符串字面值(string literals),因此regexp包提供了包裝函數regexp.MustCompile檢查輸入的合法性。

package regexp
func Compile(expr string) (*Regexp, error) { /* ... */ }
func MustCompile(expr string) *Regexp {
    re, err := Compile(expr)
    if err != nil {
        panic(err)
    }
    return re
}

包裝函數使得調用者可以便捷的用一個編譯後的正則表達式為包級別的變量賦值:

var httpSchemeRE = regexp.MustCompile(`^https?:`) //"http:" or "https:"

顯然,MustCompile不能接收不合法的輸入。函數名中的Must前綴是一種針對此類函數的命名約定,比如template.Must(4.6節)

func main() {
    f(3)
}
func f(x int) {
    fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
    defer fmt.Printf("defer %d\n", x)
    f(x - 1)
}

上例中的運行輸出如下:

f(3)
f(2)
f(1)
defer 1
defer 2
defer 3

當f(0)被調用時,發生panic異常,之前被延遲執行的3個fmt.Printf被調用。程序中斷執行後,panic信息和堆棧信息會被輸出(下面是簡化的輸出):

panic: runtime error: integer divide by zero
main.f(0)
src/gopl.io/ch5/defer1/defer.go:14
main.f(1)
src/gopl.io/ch5/defer1/defer.go:16
main.f(2)
src/gopl.io/ch5/defer1/defer.go:16
main.f(3)
src/gopl.io/ch5/defer1/defer.go:16
main.main()
src/gopl.io/ch5/defer1/defer.go:10

我們在下一節將看到,如何使程序從panic異常中恢復,阻止程序的崩潰。

為了方便診斷問題,runtime包允許程序員輸出堆棧信息。在下面的例子中,我們通過在main函數中延遲調用printStack輸出堆棧信息。

gopl.io/ch5/defer2

func main() {
    defer printStack()
    f(3)
}
func printStack() {
    var buf [4096]byte
    n := runtime.Stack(buf[:], false)
    os.Stdout.Write(buf[:n])
}

printStack的簡化輸出如下(下面只是printStack的輸出,不包括panic的日誌信息):

goroutine 1 [running]:
main.printStack()
src/gopl.io/ch5/defer2/defer.go:20
main.f(0)
src/gopl.io/ch5/defer2/defer.go:27
main.f(1)
src/gopl.io/ch5/defer2/defer.go:29
main.f(2)
src/gopl.io/ch5/defer2/defer.go:29
main.f(3)
src/gopl.io/ch5/defer2/defer.go:29
main.main()
src/gopl.io/ch5/defer2/defer.go:15

將panic機制類比其他語言異常機制的讀者可能會驚訝,runtime.Stack為何能輸出已經被釋放函數的信息?在Go的panic機制中,延遲函數的調用在釋放堆棧信息之前。