這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一、前言
在實際的項目中,對於異常的最佳實務很多,在使用不同的語言開發不同類型的程式時,有不同的建議。Google C++ Style 中提到 Google 內部的 C++ 代碼中不使用異常,社區也有很多關於異常的討論。
作為一門相對來說很新的語言,Go語言中沒有使用傳統的 try...catch 類似的異常處理機制,而是提供了 panic 和 recover 函數來處理所謂的運行時異常,也就是 Google 所稱的錯誤處理機制。配合 defer 語句和 error 介面開發人員可以非常靈活地處理運行時的錯誤和異常。Google 肯定不希望開發人員在代碼中隨便使用 panic,我們知道在風險控制中,有所謂 已知的未知 和 未知的未知,在 Go 程式設計中,對於前者,我們可以通過預先開發的代碼分支來處理,對於後者就比較棘手了:
1、如果項目中的代碼、使用的標準庫以及第三方庫在運行時內部捕獲了異常並通過合適的 error 對象返回給調用者,那我們可以盡量少甚至可以不用 panic 函數。
2、如果無法保證上面的情況,那為了確保程式在運行時不會因為 未知的未知 導致崩潰,那 panic 函數的使用可能不得不加在任何需要的地方。
我們應該認識到,panic是我們和電腦都不希望看到的,應該在設計開發的時候充分考慮使用情境可能出現的情況,處理好 已知的未知,在確定需要的地方使用 panic 機制。
二、defer
defer關鍵字用來標記最後執行的Go語句,一般用在資源釋放、關閉串連等操作,會在函數關閉前調用。
多個defer的定義與執行類似於棧的操作:先進後出,最先定義的最後執行。
請先看下邊幾段代碼,然後判斷一下各自輸出內容:
// 範例程式碼一:func funcA() int { x := 5 defer func() { x += 1 }() return x}// 範例程式碼二:func funcB() (x int) { defer func() { x += 1 }() return 5}// 範例程式碼三:func funcC() (y int) { x := 5 defer func() { x += 1 }() return x} // 範例程式碼四:func funcD() (x int) { defer func(x int) { x += 1 }(x) return 5}
解析這幾段代碼,主要需要理解清楚以下幾點知識:
1、return語句的處理過程
return xxx 語句並不是一條原子指令,其在執行的時候會進行語句分解成 返回變數=xxx return,最後執行return
2、defer語句執行時機
上文說過,defer語句是在函數關閉的時候調用,確切的說是在執行return語句的時候調用,注意,是return 不是return xxx
3、函數參數的傳遞方式
Go語言中普通的函數參數的傳遞方式是值傳遞,即新辟記憶體拷貝變數值,不包括slice和map,這兩種類型是引用傳遞
4、變數賦值的傳遞方式
Go語言變數的賦值跟函數參數類似,也是值拷貝,不包括slice和map,這兩種類型是記憶體引用
按照以上原則,解析代碼:
// 解析代碼一:返回temp的值,在將x賦值給temp後,temp未發生改變,最終傳回值為5func funcA() int { x := 5 temp=x #temp變數表示未顯示聲明的return變數 func() { x += 1 }() return}// 解析代碼二:返回x的值,先對其複製5,接著函數中改變為6,最終傳回值為6func funcB() (x int) { x = 5 func() { x += 1 }() return}// 解析代碼三:返回y的值,在將x賦值給y後,y未發生改變,最終傳回值為5func funcC() (y int) { x := 5 y = x #這裡是值拷貝 func() { x += 1 }() return}// 解析代碼四:返回x的值,傳遞x到匿名函數中執行時,傳遞的是x的拷貝,不影響外部x的值,最終傳回值為5func funcD() (x int) { x := 5 func(x int) { #這裡是值拷貝 x += 1 }(x) return}
三、Error
Go語言 通過支援多傳回值,讓在運行時返回詳細的錯誤資訊給調用者變得非常方便。我們可以在編碼中通過實現 error 介面類型來建置錯誤資訊,error 介面的定義如下:
type error interface { Error() string}
還是通過下面的例子來看看:
package mainimport ("fmt")// 定義一個 DivideError 結構type DivideError struct {dividee intdivider int}// 實現 `error` 介面func (de *DivideError) Error() string {strFormat := `Cannot proceed, the divider is zero.dividee: %ddivider: 0`return fmt.Sprintf(strFormat, de.dividee)}// 定義 `int` 類型除法運算的函數func Divide(varDividee int, varDivider int) (result int, errorMsg string) {if varDivider == 0 {dData := DivideError{dividee: varDividee,divider: varDivider,}errorMsg = dData.Error()return} else {return varDividee / varDivider, ""}}func main() {// 正常情況if result, errorMsg := Divide(100, 10); errorMsg == "" {fmt.Println("100/10 = ", result)}// 當被除數為零的時候會返回錯誤資訊if _, errorMsg := Divide(100, 0); errorMsg != "" {fmt.Println("errorMsg is: ", errorMsg)}}
運行後可以看到下面的輸出:
100/10 = 10errorMsg is: Cannot proceed, the divider is zero.dividee: 100divider: 0
四、Panic 和 recover
定義如下:
func panic(interface{})func recover() interface{}
panic 和 recover 是兩個內建函數,用於處理 run-time panics 以及程式中自訂的錯誤。
當執行一個函數 F 的時候,如果顯式地調用 panic 函數或者一個 run-time panics 發生時,F 會結束運行,所有 F 中 defer 的函數會按照 FILO 的規則被執行。之後,F 函數的調用者中 defer 的函數再被執行,如此一直到最外層代碼。這時,程式已經被中斷了而且錯誤也被一層層拋出來了,其中包括 panic 函數的參數。當前被中斷的 goroutine 被稱為處於 panicking 狀態。由於 panic 函數的參數是空介面類型,所以可以接受任何類型的對象:
panic(42)panic(42)panic("unreachable")panic(Error("cannot parse"))
recover 函數用來擷取 panic 函數的參數資訊,只能在延時調用 defer 語句調用的函數中直接調用才會生效,如果在 defer 語句中也調用 panic 函數,則只有最後一個被調用的 panic 函數的參數會被 recover 函數擷取到。如果 goroutine 沒有 panic,那調用 recover 函數會返回 nil。
package mainimport ("fmt")// 最簡單的例子func SimplePanicRecover() {defer func() {if err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}()panic("SimplePanicRecover function panic-ed!")}// 當 defer 中也調用了 panic 函數時,最後被調用的 panic 函數的參數會被後面的 recover 函數擷取到// 一個函數中可以定義多個 defer 函數,按照 FILO 的規則執行func MultiPanicRecover() {defer func() {if err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}()defer func() {panic("MultiPanicRecover defer inner panic")}()defer func() {if err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}()panic("MultiPanicRecover function panic-ed!")}// recover 函數只有在 defer 函數中被直接調用的時候才可以擷取 panic 的參數func RecoverPlaceTest() {// 下面一行代碼中 recover 函數會返回 nil,但也不影響程式運行defer recover()// recover 函數返回 nildefer fmt.Println("recover() is: ", recover())defer func() {func() {// 由於不是在 defer 調用函數中直接調用 recover 函數,recover 函數會返回 nilif err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}()}()defer func() {if err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}()panic("RecoverPlaceTest function panic-ed!")}// 如果函數沒有 panic,調用 recover 函數不會擷取到任何資訊,也不會影響當前進程。func NoPanicButHasRecover() {if err := recover(); err != nil {fmt.Println("NoPanicButHasRecover Panic info is: ", err)} else {fmt.Println("NoPanicButHasRecover Panic info is: ", err)}}// 定義一個調用 recover 函數的函數func CallRecover() {if err := recover(); err != nil {fmt.Println("Panic info is: ", err)}}// 定義個函數,在其中 defer 另一個調用了 recover 函數的函數func RecoverInOutterFunc() {defer CallRecover()panic("RecoverInOutterFunc function panic-ed!")}func main() {SimplePanicRecover()MultiPanicRecover()RecoverPlaceTest()NoPanicButHasRecover()RecoverInOutterFunc()}
運行後可以看到下面的輸出:
Panic info is: SimplePanicRecover function panic-ed!Panic info is: MultiPanicRecover function panic-ed!Panic info is: MultiPanicRecover defer inner panicPanic info is: RecoverPlaceTest function panic-ed!recover() is: <nil>NoPanicButHasRecover Panic info is: <nil>Panic info is: RecoverInOutterFunc function panic-ed!