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


22 | panic函數、recover函數以及defer語句(下)

我在前一篇文章提到過這樣一個說法,panic 之中可以包含一個值,用於簡要解釋引發此 panic 的原因。

如果一個 panic 是我們在無意間引發的,那么其中的值只能由 Go 語言運行時系統給定。但是,當我們使用panic函數有意地引發一個 panic 的時候,卻可以自行指定其包含的值。我們今天的第一個問題就是針對后一種情況提出的。

知識擴展

問題 1:怎樣讓 panic 包含一個值,以及應該讓它包含什么樣的值?

這其實很簡單,在調用panic函數時,把某個值作為參數傳給該函數就可以了。由於panic函數的唯一一個參數是空接口(也就是interface{})類型的,所以從語法上講,它可以接受任何類型的值。

但是,我們最好傳入error類型的錯誤值,或者其他的可以被有效序列化的值。這里的“有效序列化”指的是,可以更易讀地去表示形式轉換。

還記得嗎?對於fmt包下的各種打印函數來說,error類型值的Error方法與其他類型值的String方法是等價的,它們的唯一結果都是string類型的。

我們在通過占位符%s打印這些值的時候,它們的字符串表示形式分別都是這兩種方法產出的。

一旦程序異常了,我們就一定要把異常的相關信息記錄下來,這通常都是記到程序日志里。

我們在為程序排查錯誤的時候,首先要做的就是查看和解讀程序日志;而最常用也是最方便的日志記錄方式,就是記下相關值的字符串表示形式。

所以,如果你覺得某個值有可能會被記到日志里,那么就應該為它關聯String方法。如果這個值是error類型的,那么讓它的Error方法返回你為它定制的字符串表示形式就可以了。

對於此,你可能會想到fmt.Sprintf,以及fmt.Fprintf這類可以格式化並輸出參數的函數。

是的,它們本身就可以被用來輸出值的某種表示形式。不過,它們在功能上,肯定遠不如我們自己定義的Error方法或者String方法。因此,為不同的數據類型分別編寫這兩種方法總是首選。

可是,這與傳給panic函數的參數值又有什么關系呢?其實道理是相同的。至少在程序崩潰的時候,panic 包含的那個值字符串表示形式會被打印出來。

另外,我們還可以施加某種保護措施,避免程序的崩潰。這個時候,panic 包含的值會被取出,而在取出之后,它一般都會被打印出來或者記錄到日志里。

既然說到了應對 panic 的保護措施,我們再來看下面一個問題。

問題 2:怎樣施加應對 panic 的保護措施,從而避免程序崩潰?

Go 語言的內建函數recover專用於恢復 panic,或者說平息運行時恐慌。recover函數無需任何參數,並且會返回一個空接口類型的值。

如果用法正確,這個值實際上就是即將恢復的 panic 包含的值。並且,如果這個 panic 是因我們調用panic函數而引發的,那么該值同時也會是我們此次調用panic函數時,傳入的參數值副本。請注意,這里強調用法的正確。我們先來看看什么是不正確的用法。

package main

import (
 "fmt"
 "errors"
)

func main() {
 fmt.Println("Enter function main.")
 // 引發panic。
 panic(errors.New("something wrong"))
 p := recover()
 fmt.Printf("panic: %s\n", p)
 fmt.Println("Exit function main.")
}

在上面這個main函數中,我先通過調用panic函數引發了一個 panic,緊接着想通過調用recover函數恢復這個 panic。可結果呢?你一試便知,程序依然會崩潰,這個recover函數調用並不會起到任何作用,甚至都沒有機會執行。

還記得嗎?我提到過 panic 一旦發生,控制權就會訊速地沿着調用棧的反方向傳播。所以,在panic函數調用之后的代碼,根本就沒有執行的機會。

那如果我把調用recover函數的代碼提前呢?也就是說,先調用recover函數,再調用panic函數會怎么樣呢?

這顯然也是不行的,因為,如果在我們調用recover函數時未發生 panic,那么該函數就不會做任何事情,並且只會返回一個nil。

換句話說,這樣做毫無意義。那么,到底什么才是正確的recover函數用法呢?這就不得不提到defer語句了。

顧名思義,defer語句就是被用來延遲執行代碼的。延遲到什么時候呢?這要延遲到該語句所在的函數即將執行結束的那一刻,無論結束執行的原因是什么。

這與go語句有些類似,一個defer語句總是由一個defer關鍵字和一個調用表達式組成。

這里存在一些限制,有一些調用表達式是不能出現在這里的,包括:針對 Go 語言內建函數的調用表達式,以及針對unsafe包中的函數的調用表達式。

順便說一下,對於go語句中的調用表達式,限制也是一樣的。另外,在這里被調用的函數可以是有名稱的,也可以是匿名的。我們可以把這里的函數叫做defer函數或者延遲函數。注意,被延遲執行的是defer函數,而不是defer語句。

我剛才說了,無論函數結束執行的原因是什么,其中的defer函數調用都會在它即將結束執行的那一刻執行。即使導致它執行結束的原因是一個 panic 也會是這樣。正因為如此,我們需要聯用defer語句和recover函數調用,才能夠恢復一個已經發生的 panic。

我們來看一下經過修正的代碼。

package main

import (
 "fmt"
 "errors"
)

func main() {
 fmt.Println("Enter function main.")
 defer func(){
  fmt.Println("Enter defer function.")
  if p := recover(); p != nil {
   fmt.Printf("panic: %s\n", p)
  }
  fmt.Println("Exit defer function.")
 }()
 // 引發panic。
 panic(errors.New("something wrong"))
 fmt.Println("Exit function main.")
}

在這個main函數中,我先編寫了一條defer語句,並在defer函數中調用了recover函數。僅當調用的結果值不為nil時,也就是說只有 panic 確實已發生時,我才會打印一行以“panic:”為前綴的內容。

緊接着,我調用了panic函數,並傳入了一個error類型值。這里一定要注意,我們要盡量把defer語句寫在函數體的開始處,因為在引發 panic 的語句之后的所有語句,都不會有任何執行機會。

也只有這樣,defer函數中的recover函數調用才會攔截,並恢復defer語句所屬的函數,及其調用的代碼中發生的所有 panic。

至此,我向你展示了兩個很典型的recover函數的錯誤用法,以及一個基本的正確用法。

我希望你能夠記住錯誤用法背后的緣由,同時也希望你能真正地理解聯用defer語句和recover函數調用的真諦。

在命令源碼文件 demo50.go 中,我把上述三種用法合並在了一段代碼中。你可以運行該文件,並體會各種用法所產生的不同效果。

package main

import (
	"errors"
	"fmt"
)

func main() {
	fmt.Println("Enter function main.")

	defer func() {
		fmt.Println("Enter defer function.")

		// recover函數的正確用法。
		if p := recover(); p != nil {
			fmt.Printf("panic: %s\n", p)
		}

		fmt.Println("Exit defer function.")
	}()

	// recover函數的錯誤用法。
	fmt.Printf("no panic: %v\n", recover())

	// 引發panic。
	panic(errors.New("something wrong"))

	// recover函數的錯誤用法。
	p := recover()
	fmt.Printf("panic: %s\n", p)

	fmt.Println("Exit function main.")
}

下面我再來多說一點關於defer語句的事情。

問題 3:如果一個函數中有多條defer語句,那么那幾個defer函數調用的執行順序是怎樣的?

如果只用一句話回答的話,那就是:在同一個函數中,defer函數調用的執行順序與它們分別所屬的defer語句的出現順序(更嚴謹地說,是執行順序)完全相反。

當一個函數即將結束執行時,其中的寫在最下邊的defer函數調用會最先執行,其次是寫在它上邊、與它的距離最近的那個defer函數調用,以此類推,最上邊的defer函數調用會最后一個執行。

如果函數中有一條for語句,並且這條for語句中包含了一條defer語句,那么,顯然這條defer語句的執行次數,就取決於for語句的迭代次數。

並且,同一條defer語句每被執行一次,其中的defer函數調用就會產生一次,而且,這些函數調用同樣不會被立即執行。

那么問題來了,這條for語句中產生的多個defer函數調用,會以怎樣的順序執行呢?

為了徹底搞清楚,我們需要弄明白defer語句執行時發生的事情。

其實也並不復雜,在defer語句每次執行的時候,Go 語言會把它攜帶的defer函數及其參數值另行存儲到一個鏈表中。

這個鏈表與該defer語句所屬的函數是對應的,並且,它是先進后出(FILO)的,相當於一個棧。

在需要執行某個函數中的defer函數調用的時候,Go 語言會先拿到對應的鏈表,然后從該鏈表中一個一個地取出defer函數及其參數值,並逐個執行調用。

這正是我說“defer函數調用與其所屬的defer語句的執行順序完全相反”的原因了。

下面該你出場了,我在 demo51.go 文件中編寫了一個與本問題有關的示例,其中的核心代碼很簡單,只有幾行而已。

package main

import "fmt"

func main() {
	defer fmt.Println("first defer")
	for i := 0; i < 3; i++ {
		defer fmt.Printf("defer in for [%d]\n", i)
	}
	defer fmt.Println("last defer")
}

總結

我們這兩期的內容主要講了兩個函數和一條語句。recover函數專用於恢復 panic,並且調用即恢復。

它在被調用時會返回一個空接口類型的結果值。如果在調用它時並沒有 panic 發生,那么這個結果值就會是nil。

而如果被恢復的 panic 是我們通過調用panic函數引發的,那么它返回的結果值就會是我們傳給panic函數參數值的副本。

對recover函數的調用只有在defer語句中才能真正起作用。defer語句是被用來延遲執行代碼的。

更確切地說,它會讓其攜帶的defer函數的調用延遲執行,並且會延遲到該defer語句所屬的函數即將結束執行的那一刻。

在同一個函數中,延遲執行的defer函數調用,會與它們分別所屬的defer語句的執行順序完全相反。還要注意,同一條defer語句每被執行一次,就會產生一個延遲執行的defer函數調用。

這種情況在defer語句與for語句聯用時經常出現。這時更要關注for語句中,同一條defer語句產生的多個defer函數調用的實際執行順序。

以上這些,就是關於 Go 語言中特殊的程序異常,及其處理方式的核心知識。這里邊可以衍生出很多面試題目。

思考題

我們可以在defer函數中恢復 panic,那么可以在其中引發 panic 嗎?

筆記源碼

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

知識共享許可協議

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

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


免責聲明!

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



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