Go 語(yǔ)言 Panic異常

2023-03-14 16:54 更新

原文鏈接:https://gopl-zh.github.io/ch5/ch5-09.html


5.9. Panic異常

Go的類型系統(tǒng)會(huì)在編譯時(shí)捕獲很多錯(cuò)誤,但有些錯(cuò)誤只能在運(yùn)行時(shí)檢查,如數(shù)組訪問(wèn)越界、空指針引用等。這些運(yùn)行時(shí)錯(cuò)誤會(huì)引起panic異常。

一般而言,當(dāng)panic異常發(fā)生時(shí),程序會(huì)中斷運(yùn)行,并立即執(zhí)行在該goroutine(可以先理解成線程,在第8章會(huì)詳細(xì)介紹)中被延遲的函數(shù)(defer 機(jī)制)。隨后,程序崩潰并輸出日志信息。日志信息包括panic value和函數(shù)調(diào)用的堆棧跟蹤信息。panic value通常是某種錯(cuò)誤信息。對(duì)于每個(gè)goroutine,日志信息中都會(huì)有與之相對(duì)的,發(fā)生panic時(shí)的函數(shù)調(diào)用堆棧跟蹤信息。通常,我們不需要再次運(yùn)行程序去定位問(wèn)題,日志信息已經(jīng)提供了足夠的診斷依據(jù)。因此,在我們填寫(xiě)問(wèn)題報(bào)告時(shí),一般會(huì)將panic異常和日志信息一并記錄。

不是所有的panic異常都來(lái)自運(yùn)行時(shí),直接調(diào)用內(nèi)置的panic函數(shù)也會(huì)引發(fā)panic異常;panic函數(shù)接受任何值作為參數(shù)。當(dāng)某些不應(yīng)該發(fā)生的場(chǎng)景發(fā)生時(shí),我們就應(yīng)該調(diào)用panic。比如,當(dāng)程序到達(dá)了某條邏輯上不可能到達(dá)的路徑:

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

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

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

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

考慮regexp.Compile函數(shù),該函數(shù)將正則表達(dá)式編譯成有效的可匹配格式。當(dāng)輸入的正則表達(dá)式不合法時(shí),該函數(shù)會(huì)返回一個(gè)錯(cuò)誤。當(dāng)調(diào)用者明確的知道正確的輸入不會(huì)引起函數(shù)錯(cuò)誤時(shí),要求調(diào)用者檢查這個(gè)錯(cuò)誤是不必要和累贅的。我們應(yīng)該假設(shè)函數(shù)的輸入一直合法,就如前面的斷言一樣:當(dāng)調(diào)用者輸入了不應(yīng)該出現(xiàn)的輸入時(shí),觸發(fā)panic異常。

在程序源碼中,大多數(shù)正則表達(dá)式是字符串字面值(string literals),因此regexp包提供了包裝函數(shù)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
}

包裝函數(shù)使得調(diào)用者可以便捷的用一個(gè)編譯后的正則表達(dá)式為包級(jí)別的變量賦值:

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

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

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)
}

上例中的運(yùn)行輸出如下:

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

當(dāng)f(0)被調(diào)用時(shí),發(fā)生panic異常,之前被延遲執(zhí)行的3個(gè)fmt.Printf被調(diào)用。程序中斷執(zhí)行后,panic信息和堆棧信息會(huì)被輸出(下面是簡(jiǎn)化的輸出):

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

我們?cè)谙乱还?jié)將看到,如何使程序從panic異常中恢復(fù),阻止程序的崩潰。

為了方便診斷問(wèn)題,runtime包允許程序員輸出堆棧信息。在下面的例子中,我們通過(guò)在main函數(shù)中延遲調(diào)用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的簡(jiǎn)化輸出如下(下面只是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機(jī)制類比其他語(yǔ)言異常機(jī)制的讀者可能會(huì)驚訝,runtime.Stack為何能輸出已經(jīng)被釋放函數(shù)的信息?在Go的panic機(jī)制中,延遲函數(shù)的調(diào)用在釋放堆棧信息之前。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)