gopl-zh.github.com/ch5/ch5-09.md

5.4 KiB
Raw Blame History

5.9. Panic異常

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

一般而言當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.Must4.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機製中延遲函數的調用在釋放堆棧信息之前。