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


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/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM