Go語言核心36講(Go語言進階技術十五)–學習筆記
- 2021 年 11 月 8 日
- 筆記
- 【016】Go語言核心36講
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
(從 panic 到程式崩潰)
好了,到這裡,我相信你已經對 panic 被引發後的程式終止過程有一定的了解了。深入地了解此過程,以及正確地解讀 panic 詳情應該是我們的必備技能,這在調試 Go 程式或者為 Go 程式排查錯誤的時候非常重要。
總結
最近的兩篇文章,我們是圍繞著 panic 函數、recover 函數以及 defer 語句進行的。今天我主要講了 panic 函數。這個函數是專門被用來引發 panic 的。panic 也可以被稱為運行時恐慌,它是一種只能在程式運行期間拋出的程式異常。
Go 語言的運行時系統可能會在程式出現嚴重錯誤時自動地拋出 panic,我們在需要時也可以通過調用panic函數引發 panic。但不論怎樣,如果不加以處理,panic 就會導致程式崩潰並終止運行。
思考題
一個函數怎樣才能把 panic 轉化為error類型值,並將其作為函數的結果值返回給調用方?
筆記源碼
//github.com/MingsonZheng/go-core-demo
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: //www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。