Go 語言異常處理

FunTester發表於2024-08-22

JavaC# 等程式語言中,錯誤處理通常是透過 try-catch 機制來管理的。當程式在 try 塊中遇到錯誤時,catch 塊會捕獲該錯誤,並執行相應的處理邏輯。這種機制為處理異常提供了一種結構化的方法,確保即使在發生錯誤的情況下,應用程式也不會意外崩潰。

與此不同,Go 語言採用了一種完全不同的錯誤處理方式。在 Go 中,沒有傳統意義上的異常處理機制。相反,Go 將錯誤視為函式的返回值之一。這意味著在呼叫函式後,開發者需要主動檢查是否返回了錯誤,並根據情況決定如何處理它。這種方法更加強調顯式的錯誤處理,而不是像 try-catch 那樣隱式的異常處理。這不僅使程式碼邏輯更為清晰,還鼓勵了更好的錯誤管理實踐。

下面我們將探討幾種在 Go 語言中處理錯誤的技巧,幫助你更輕鬆地管理程式碼中的錯誤。

內建 errors

Go 語言的錯誤處理機制以其內建的 error 型別為基礎。error 型別是一個介面,它只有一個方法:Error() 。這個方法返回一個描述錯誤的字串。任何實現了 Error() 方法的型別都被視為 error 型別。這種設計允許開發者輕鬆建立自己的自定義錯誤型別,以適應具體的需求。

這種簡單而靈活的設計,使得 Go 的錯誤型別既強大又易於使用。它完全符合 Go 語言的設計哲學:直觀簡潔。透過這種方式,錯誤處理可以自然地融入到 Go 程式碼的整體結構中,不僅保持了程式碼的簡潔性,還確保了錯誤處理的有效性。

下面是 error 介面的定義,PS:後面自定義錯誤型別也會用到這個。

type error interface {
    Error() string
}

Go 標準庫中的 errors 包提供了一種簡單的方法來建立錯誤例項,使用的是 errors.New() 函式。這個函式接受一個字串引數,並返回一個包含該訊息的錯誤物件。由於這種方式非常直接,它鼓勵開發者建立易於理解的錯誤訊息,使得錯誤定位和處理更加簡單明瞭。

Go 語言中,error 型別的使用通常伴隨著函式呼叫後的即時檢查。這種方式要求開發者在每次函式呼叫後都要明確地檢查是否發生了錯誤。這不僅可以確保在錯誤發生時立即處理,還能防止錯誤在未被察覺的情況下傳播,進而避免在執行流程的後期引發更嚴重的問題。

這種顯式的錯誤處理方法雖然要求在程式碼中多寫幾行,但它帶來了更高的程式碼可讀性和更少的隱藏錯誤的風險,使程式更加健壯和可靠。

下面是一個示例,說明如何實現除法運算的錯誤處理,具體解決除以零的情況。

package main  

import (  
    "errors"  
    "fmt")  

func divide(a, b int) (int, error) {  
    if b == 0 {  
       return 0, errors.New("被除數不能為零, 請檢查")  
    }  
    return a / b, nil  
}  

func main() {  
    result, err := divide(4, 0)  
    if err != nil {  
       fmt.Println("Error:", err)  
       return  
    }  
    fmt.Println("Result:", result)  
}

控制檯列印結果 Error: 被除數不能為零, 請檢查

在這個示例中,divide 函式首先檢查除數 b 是否為 0。如果是,則透過 errors.New() 函式建立一個錯誤,並返回給呼叫者。同時,函式返回一個預設值 0,表示運算失敗。反之,如果 b 不為 0,函式則返回 a 除以 b 的結果,以及 nil 表示沒有錯誤。

main 函式中,divide 被呼叫,並檢查返回的 error 是否為 nil。如果發生錯誤(即 b 為 0),程式將輸出錯誤資訊並退出。否則,程式將繼續執行並輸出除法結果。

包裝 error

錯誤包裝是一種為錯誤資訊新增額外上下文的技術,使錯誤更容易除錯和理解。透過為錯誤新增更多的上下文資訊,開發者能夠更準確地判斷錯誤發生的原因和位置,這在複雜的程式碼庫中尤為重要。

在 Go 語言的早期版本中,雖然可以透過拼接字串的方式新增上下文,但這種方法通常會丟失原始錯誤的具體資訊。為了解決這一問題,Go 1.13 引入了 fmt.Errorf() 函式,使得錯誤包裝變得更加簡潔和強大。

fmt.Errorf() 函式允許在保留原始錯誤的同時,為其新增更多有用的上下文資訊。這樣不僅可以將錯誤傳遞給上層呼叫者,還能為錯誤資訊提供更多細節,以便在出現問題時更容易定位問題的根源。透過這種方式,錯誤包裝能夠顯著提高程式碼的可維護性和除錯效率。

我們可以這樣使用 fmt.Errorf() 進行錯誤包裝:

package main  

import (  
    "errors"  
    "fmt")  

func divide(a, b int) (int, error) {  
    if b == 0 {  
        return 0, fmt.Errorf("除法運算error,  %w", errors.New("被除數不能為0"))
    }  
    return a / b, nil  
}  

func main() {  
    result, err := divide(4, 0)  
    if err != nil {  
       fmt.Println("Error:", err)  
       return  
    }  
    fmt.Println("結果:", result)  
}

控制檯列印:

Error: 除法運算error,  被除數不能為0

錯誤包裝在開發中發揮了重要作用,尤其是在構建更具資訊量的錯誤報告系統時。透過在錯誤資訊中加入詳細的上下文,您可以顯著提高系統的可讀性和除錯效率。這種做法不僅能使錯誤資訊更加直觀,還能提供關於錯誤發生的情況和潛在失敗原因的寶貴見解。

具體來說,當錯誤資訊包含了更多背景資訊時,開發者可以更容易地理解錯誤的上下文,從而迅速定位問題的根源。例如,錯誤包裝可以顯示錯誤發生的函式名、引數值以及導致錯誤的具體條件。這種細緻的資訊有助於在除錯過程中快速發現並修復問題,減少了排查錯誤的時間。

此外,錯誤包裝還可以幫助團隊成員之間更好地溝通和協作。詳細的錯誤資訊使得團隊在討論問題時可以更精確地描述問題的性質,從而更高效地制定解決方案。這種透明的錯誤報告方式對於長期維護和迭代開發尤為重要,它使得系統的錯誤處理更加可靠和易於管理。

自定義 error

建立自定義錯誤型別允許您為錯誤提供額外的上下文和功能,從而使錯誤處理更加靈活和有用。當實現 error 介面時,您可以構建更復雜的錯誤型別,提供對錯誤的詳細見解,這在需要錯誤訊息之外的其他資訊時特別有用。

透過定義自定義錯誤型別,您可以將錯誤資訊與其他相關的資料和行為結合起來。這種方法使您能夠建立具有特定屬性和方法的錯誤型別,使得錯誤報告更加豐富和精確。例如,您可以定義一個錯誤型別,該型別不僅包含錯誤訊息,還包括錯誤程式碼、發生錯誤的時間、或額外的上下文資訊。這樣,當錯誤發生時,您可以獲得更全面的錯誤資訊,幫助更好地理解問題的背景和解決方案。

下面是一個演示的例子:

package main  

import (  
    "fmt"  
)  

type FunTesterError struct {  
    Reason string  
}  

func (e *FunTesterError) Error() string {  
    return fmt.Sprintf("%s is fun", e.Reason)  
}  

func divide(a, b int) (int, error) {  
    if b == 0 {  
       return 0, &FunTesterError{Reason: "I am fun"}  
    }  

    return a / b, nil  
}  

func main() {  
    data, err := divide(8, 0)  
    if err != nil {  
       fmt.Println("Error:", err)  
       return  
    }  
    fmt.Println("結果:", data)  
}

這段 Go 程式碼演示瞭如何使用自定義錯誤型別來改進錯誤處理。首先,定義了一個 FunTesterError 型別,它實現了 error 介面,幷包含一個 Reason 欄位。Error() 方法返回格式化的錯誤資訊。divide 函式嘗試對兩個整數進行除法運算,如果除數 b 為 0,則返回一個 FunTesterError 錯誤。main 函式呼叫 divide 函式,並根據是否返回錯誤來輸出相應的錯誤資訊或運算結果。當除數為 0 時,錯誤訊息 "I am fun is fun" 會被列印。這樣,自定義錯誤型別幫助提供了更具描述性的錯誤資訊,便於除錯和理解。

error 日誌

記錄錯誤是除錯和監控應用程式的關鍵實踐之一。Go 語言中的 log 包提供了一種簡單而有效的方法來記錄錯誤和其他重要訊息。

透過記錄錯誤,您可以實時監控應用程式的狀態,及時發現並響應出現的問題。此外,記錄的錯誤資訊可以幫助您識別可能預示更深層次問題的模式,從而採取預防措施。這樣的記錄機制對於維持應用程式的健康和效能尤為重要,尤其是在生產環境中,它能幫助您跟蹤和解決潛在的故障,確保應用程式的穩定性和可靠性。

日誌記錄實現的庫比較多,我就用內建的 log 來列印日誌了。

package main  

import (  
    "errors"  
    "fmt"
    "log")  

func divide(a, b int) (int, error) {  
    if b == 0 {  
       return 0, errors.New("被除數不能為零, 請檢查")  
    }  
    return a / b, nil  
}  

func main() {  
    result, err := divide(4, 0)  
    if err != nil {  
       log.Printf("Error: %s\n", err)  
       return  
    }  
    fmt.Println("Result:", result)  
}

此處省略控制檯資訊。

記錄錯誤不僅有助於除錯和監控應用程式,還對主動監控和警報系統至關重要。透過日誌分析,您可以識別潛在的問題模式和趨勢,及時採取措施進行干預。這種方法使您能夠在問題惡化之前進行修復,從而減少停機時間並提升使用者體驗。

詳細的錯誤日誌可以幫助您發現系統中的異常行為、效能瓶頸或其他潛在故障,並觸發預設的警報。這種主動監控機制使您能夠在問題發生初期做出響應,避免嚴重的系統故障,確保應用程式的平穩執行和使用者的持續滿意。

panic 和 recovery

雖然 Go 的主要錯誤處理機制依賴於將錯誤作為返回值處理,但 Go 語言也提供了 panicrecover 機制,用於處理無法透過常規錯誤處理解決的異常情況。

panic 用於處理程式遇到的不可恢復的錯誤或嚴重故障,例如程式設計錯誤或致命錯誤。當 panic 被觸發時,程式的正常執行將被中斷,控制權會轉移到最接近的 defer 語句,進行資源清理,然後程式終止執行。這種機制主要用於處理那些程式無法繼續執行的情況,如陣列越界或空指標引用等嚴重錯誤。

不過,Go 還提供了 recover 函式,用於從 panic 中恢復控制權。recover 只能在 defer 呼叫中使用,它可以捕獲 panic 產生的異常,使程式能夠在出現嚴重錯誤時以受控的方式繼續執行。透過這種機制,開發者可以處理意外的崩潰並恢復程式的正常狀態,從而提高程式的健壯性和穩定性。

panic

在 Go 語言中,panic 是一個內建函式,用於立即停止程式的正常控制流。當 panic 被觸發時,程式會立即中斷當前函式的執行,開始展開呼叫堆疊,並執行所有沿途的 defer 函式。這種機制用於處理嚴重錯誤或異常情況,確保程式在遇到無法繼續執行的錯誤時能夠及時停止。

具體來說,當函式呼叫 panic 時:

  1. 當前函式的執行會被立即停止。
  2. 程式會開始逐層展開堆疊,依次執行每個堆疊幀中的 defer 語句。這些 defer 語句通常用於清理資源或執行必要的清理工作。
  3. 如果 panic 沒有被 recover 捕獲,程式將繼續向上層堆疊展開,直到程式終止。最終,程式會輸出堆疊跟蹤資訊,這對除錯非常有用,幫助開發者定位和解決引發 panic 的根本原因。

這種機制允許開發者在遇到無法恢復的錯誤時,快速停止程式並進行除錯,同時提供有用的錯誤上下文和堆疊資訊。然而,應謹慎使用 panic,通常僅在遇到真正無法恢復的錯誤時使用,日常錯誤處理應優先依賴於返回值和 error 型別。

下面是個使用 panic 的例子:

package main  

import (  
    "fmt"  
)  

func divide(a, b int) int {  
    if b == 0 {  
       panic("被除數不能為零, 請檢查")  
    }  
    return a / b  
}  

func main() {  
    fmt.Println(divide(4, 0))  
}

當我們執行次程式碼時,程式會執行 panic 方法,導致執行中斷。下面是控制檯列印資訊:

panic: 被除數不能為零, 請檢查

goroutine 1 [running]:
main.divide(...)
        /Users/oker/GolandProjects/funtester/test/ttt/main.go:9
main.main()
        /Users/oker/GolandProjects/funtester/test/ttt/main.go:15 +0x30

recover

為了處理 panic 並允許程式在遇到嚴重錯誤後繼續執行,Go 提供了 recover() 函式。recover 只能在 defer 函式中使用,它允許在 panic 發生後恢復控制權,從而防止程式意外終止。

具體使用方式如下:

  1. 定義 defer 函式:在 defer 函式中呼叫 recover()defer 確保這些函式在當前函式的執行結束時被呼叫,無論是正常返回還是因 panic 中斷。
  2. 呼叫 recover 捕獲 panic:在 defer 函式內部呼叫 recover(),它將檢查是否有 panic 發生。如果有,recover 會捕獲到 panic 的值,並恢復程式的正常控制流。
  3. 防止程式終止:透過 recover 捕獲到 panic 後,程式可以繼續執行而不會終止。這使得程式在遇到不可預見的錯誤時,能夠進行必要的清理或執行後續操作。

以下是一個示例程式碼,展示瞭如何在 defer 函式中使用 recover 來捕獲和處理 panic

package main  

import (  
    "fmt"  
    "log")  

func divide(a, b int) int {  
    if b == 0 {  
       panic("被除數不能為零, 請檢查")  
    }  
    return a / b  
}  

func main() {  
    defer func() {  
       if r := recover(); r != nil {  
          log.Printf("從 panic 中恢復: %v", r)  
       }  
    }()  
    fmt.Println(divide(4, 0))  
    fmt.Println("程式正常退出")  
}

這個 Go 程式碼示例演示瞭如何使用 panicrecover 來處理嚴重錯誤並恢復程式的控制流。在 divide 函式中,如果除數為 0,則呼叫 panic 觸發嚴重錯誤。main 函式透過 defer 定義一個匿名函式,該函式在 panic 發生時使用 recover 捕獲錯誤,並透過 log.Printf 輸出恢復資訊。由於 panic 發生時,divide 函式中的錯誤資訊被捕獲,程式不會終止,而是繼續執行 defer 中的恢復邏輯。最終,fmt.Println("程式正常退出") 不會執行,因為 panic 打斷了正常流程。

下面是控制檯列印資訊:

 panic 中恢復: 被除數不能為零, 請檢查

panic 和 recovery 最佳實踐

panic 適用於指示程式中不可恢復的嚴重錯誤,如陣列越界、空指標引用或其他在正確程式碼中不應出現的情況。這些錯誤通常表示程式的狀態已不再有效,因此應儘快中止執行。避免將 panic 用於常規錯誤處理,因為它會中斷程式的正常流程。

recover 用於處理 panic 並允許程式在發生嚴重故障後繼續執行。在需要清理資源、記錄錯誤資訊或儘可能恢復程式狀態時,recover 提供了一個有效的機制。它應在 defer 函式中使用,以確保在 panic 發生時能夠正確捕獲和處理,避免程式直接終止。透過這種方式,您可以在發生嚴重錯誤時執行必要的清理工作,並儘可能恢復程式的正常執行。

FunTester 原創精華
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章