Go語言核心36講(Go語言進階技術十五)--學習筆記

MingsonZheng發表於2021-11-08

21 | panic函式、recover函式以及defer語句 (上)

在本篇,我要給你展示 Go 語言的另外一種錯誤處理方式。不過,嚴格來說,它處理的不是錯誤,而是異常,並且是一種在我們意料之外的程式異常。

前導知識:執行時恐慌 panic

這種程式異常被叫做 panic,我把它翻譯為執行時恐慌。其中的“恐慌”二字是由 panic 直譯過來的,而之所以前面又加上了“執行時”三個字,是因為這種異常只會在程式執行的時候被丟擲來。

我們舉個具體的例子來看看。

比如說,一個 Go 程式裡有一個切片,它的長度是 5,也就是說該切片中的元素值的索引分別為0、1、2、3、4,但是,我在程式裡卻想通過索引5訪問其中的元素值,顯而易見,這樣的訪問是不正確的。

package main

func main() {
	s1 := []int{0, 1, 2, 3, 4}
	e5 := s1[5]
	_ = e5
}

Go 程式,確切地說是程式內嵌的 Go 語言執行時系統,會在執行到這行程式碼的時候丟擲一個“index out of range”的 panic,用以提示你索引越界了。

當然了,這不僅僅是個提示。當 panic 被丟擲之後,如果我們沒有在程式裡新增任何保護措施的話,程式(或者說代表它的那個程式)就會在列印出 panic 的詳細情況(以下簡稱 panic 詳情)之後,終止執行。

現在,就讓我們來看一下這樣的 panic 詳情中都有什麼。

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
 /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.go:5 +0x3d
exit status 2

這份詳情的第一行是“panic: runtime error: index out of range”。其中的“runtime error”的含義是,這是一個runtime程式碼包中丟擲的 panic。在這個 panic 中,包含了一個runtime.Error介面型別的值。runtime.Error介面內嵌了error介面,並做了一點點擴充套件,runtime包中有不少它的實現型別。

實際上,此詳情中的“panic:”右邊的內容,正是這個 panic 包含的runtime.Error型別值的字串表示形式。

此外,panic 詳情中,一般還會包含與它的引發原因有關的 goroutine 的程式碼執行資訊。正如前述詳情中的“goroutine 1 [running]”,它表示有一個 ID 為1的 goroutine 在此 panic 被引發的時候正在執行。

注意,這裡的 ID 其實並不重要,因為它只是 Go 語言執行時系統內部給予的一個 goroutine 編號,我們在程式中是無法獲取和更改的。

我們再看下一行,“main.main()”表明了這個 goroutine 包裝的go函式就是命令原始碼檔案中的那個main函式,也就是說這裡的 goroutine 正是主 goroutine。再下面的一行,指出的就是這個 goroutine 中的哪一行程式碼在此 panic 被引發時正在執行。

這包含了此行程式碼在其所屬的原始碼檔案中的行數,以及這個原始碼檔案的絕對路徑。這一行最後的+0x3d代表的是:此行程式碼相對於其所屬函式的入口程式計數偏移量。不過,一般情況下它的用處並不大。

最後,“exit status 2”表明我的這個程式是以退出狀態碼2結束執行的。在大多數作業系統中,只要退出狀態碼不是0,都意味著程式執行的非正常結束。在 Go 語言中,因 panic 導致程式結束執行的退出狀態碼一般都會是2。

綜上所述,我們從上邊的這個 panic 詳情可以看出,作為此 panic 的引發根源的程式碼處於 demo47.go 檔案中的第 5 行,同時被包含在main包(也就是命令原始碼檔案所在的程式碼包)的main函式中。

那麼,我的第一個問題也隨之而來了。我今天的問題是:從 panic 被引發到程式終止執行的大致過程是什麼?

這道題的典型回答是這樣的。

我們先說一個大致的過程:某個函式中的某行程式碼有意或無意地引發了一個 panic。這時,初始的 panic 詳情會被建立起來,並且該程式的控制權會立即從此行程式碼轉移至呼叫其所屬函式的那行程式碼上,也就是呼叫棧中的上一級。

這也意味著,此行程式碼所屬函式的執行隨即終止。緊接著,控制權並不會在此有片刻的停留,它又會立即轉移至再上一級的呼叫程式碼處。控制權如此一級一級地沿著呼叫棧的反方向傳播至頂端,也就是我們編寫的最外層函式那裡。

這裡的最外層函式指的是go函式,對於主 goroutine 來說就是main函式。但是控制權也不會停留在那裡,而是被 Go 語言執行時系統收回。

隨後,程式崩潰並終止執行,承載程式這次執行的程式也會隨之死亡並消失。與此同時,在這個控制權傳播的過程中,panic 詳情會被逐漸地積累和完善,並會在程式終止之前被列印出來。

問題解析

panic 可能是我們在無意間(或者說一不小心)引發的,如前文所述的索引越界。這類 panic 是真正的、在我們意料之外的程式異常。不過,除此之外,我們還是可以有意地引發 panic。

Go 語言的內建函式panic是專門用於引發 panic 的。panic函式使程式開發者可以在程式執行期間報告異常。

注意,這與從函式返回錯誤值的意義是完全不同的。當我們的函式返回一個非nil的錯誤值時,函式的呼叫方有權選擇不處理,並且不處理的後果往往是不致命的。

這裡的“不致命”的意思是,不至於使程式無法提供任何功能(也可以說僵死)或者直接崩潰並終止執行(也就是真死)。

但是,當一個 panic 發生時,如果我們不施加任何保護措施,那麼導致的直接後果就是程式崩潰,就像前面描述的那樣,這顯然是致命的。

為了更清楚地展示答案中描述的過程,我編寫了 demo48.go 檔案。你可以先檢視一下其中的程式碼,再試著執行它,並體會它列印的內容所代表的含義。

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Enter function main.")
	caller1()
	fmt.Println("Exit function main.")
}

func caller1() {
	fmt.Println("Enter function caller1.")
	caller2()
	fmt.Println("Exit function caller1.")
}

func caller2() {
	fmt.Println("Enter function caller2.")
	s1 := []int{0, 1, 2, 3, 4}
	e5 := s1[5]
	_ = e5
	fmt.Println("Exit function caller2.")
}

我在這裡再提示一點。panic 詳情會在控制權傳播的過程中,被逐漸地積累和完善,並且,控制權會一級一級地沿著呼叫棧的反方向傳播至頂端。

因此,在針對某個 goroutine 的程式碼執行資訊中,呼叫棧底端的資訊會先出現,然後是上一級呼叫的資訊,以此類推,最後才是此呼叫棧頂端的資訊。

比如,main函式呼叫了caller1函式,而caller1函式又呼叫了caller2函式,那麼caller2函式中程式碼的執行資訊會先出現,然後是caller1函式中程式碼的執行資訊,最後才是main函式的資訊。

goroutine 1 [running]:
main.caller2()
 /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:22 +0x91
main.caller1()
 /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:15 +0x66
main.main()
 /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.go:9 +0x66
exit status 2

image

(從 panic 到程式崩潰)

好了,到這裡,我相信你已經對 panic 被引發後的程式終止過程有一定的瞭解了。深入地瞭解此過程,以及正確地解讀 panic 詳情應該是我們的必備技能,這在除錯 Go 程式或者為 Go 程式排查錯誤的時候非常重要。

總結

最近的兩篇文章,我們是圍繞著 panic 函式、recover 函式以及 defer 語句進行的。今天我主要講了 panic 函式。這個函式是專門被用來引發 panic 的。panic 也可以被稱為執行時恐慌,它是一種只能在程式執行期間丟擲的程式異常。

Go 語言的執行時系統可能會在程式出現嚴重錯誤時自動地丟擲 panic,我們在需要時也可以通過呼叫panic函式引發 panic。但不論怎樣,如果不加以處理,panic 就會導致程式崩潰並終止執行。

思考題

一個函式怎樣才能把 panic 轉化為error型別值,並將其作為函式的結果值返回給呼叫方?

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章