在 Go 語言中,異常處理與傳統的面嚮物件語言有所不同,主要透過返回錯誤值的方式來處理程式中的異常情況。雖然這種方式簡潔明瞭,但在實際應用中,開發者常常會忽視錯誤處理的重要性,導致程式在執行時出現潛在問題或不易察覺的漏洞。
本模組將探討 Go 語言中常見的異常處理錯誤,包括錯誤值的忽略、錯誤包裝的誤用以及錯誤的判斷邏輯等問題。透過分析這些錯誤,幫助開發者理解如何有效地處理錯誤,避免因忽略異常情況而引發的 bug 或系統崩潰。掌握良好的錯誤處理習慣,不僅能提升程式碼的健壯性,還能提高系統的穩定性和可靠性。
錯誤四十八:Panicking (#48)
示例程式碼:
package main
import (
"fmt"
"os"
)
func LoadFunTesterConfig() {
config, err := os.Open("FunTester.conf")
if err != nil {
panic(fmt.Sprintf("FunTester: 配置檔案載入失敗: %v", err))
}
defer config.Close()
// 讀取配置檔案內容
fmt.Println("FunTester: 配置檔案已載入")
}
func main() {
LoadFunTesterConfig()
fmt.Println("FunTester: 程式繼續執行")
}
錯誤說明:
在 Go 語言中,panic
用於處理不可恢復的錯誤,如程式無法繼續執行下去的嚴重問題。然而,濫用 panic
會導致程式異常終止,難以維護和測試。就像在小問題上就大喊 “救命”,不可取。
可能的影響:
使用 panic
處理可恢復的錯誤會導致程式意外中斷,影響使用者體驗和程式的穩定性。另外,過度使用 panic
會使得錯誤處理邏輯難以追蹤和維護,增加了除錯難度。
最佳實踐:
僅在遇到無法恢復的錯誤時使用 panic
,例如初始化時的重要資源失敗。另外,優先考慮使用錯誤返回值進行錯誤處理,以便呼叫者能夠根據需要決定如何應對錯誤。僅在不可挽回的情況才使用 panic
,並確保在可能的情況下使用 recover
恢復程式的正常執行。
改進後的程式碼:
使用錯誤返回值而不是 panic
:
package main
import (
"fmt"
"os"
)
func LoadFunTesterConfig() error {
config, err := os.Open("FunTester.conf")
if err != nil {
return fmt.Errorf("FunTester: 配置檔案載入失敗: %w", err)
}
defer config.Close()
// 讀取配置檔案內容
fmt.Println("FunTester: 配置檔案已載入")
return nil
}
func main() {
if err := LoadFunTesterConfig(); err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("FunTester: 程式繼續執行")
}
輸出結果:
FunTester: 配置檔案已載入
FunTester: 程式繼續執行
錯誤四十九:未考慮何時才應該包裝 error (#49)
示例程式碼:
package main
import (
"fmt"
)
func ReadFunTesterFile(filename string) error {
// 模擬讀取檔案錯誤
return fmt.Errorf("FunTester: 無法讀取檔案 %s", filename)
}
func main() {
err := ReadFunTesterFile("FunTester.txt")
if err != nil {
fmt.Printf("FunTester: 發生錯誤: %v\n", err)
// 錯誤被進一步包裝
err = fmt.Errorf("FunTester: 處理檔案時出錯: %w", err)
}
fmt.Println("FunTester: 程式結束")
}
錯誤說明:
在 Go 語言中,錯誤包裝(Wrapping error)能夠為錯誤提供上下文資訊,有助於定位問題。然而,過度或不必要地包裝錯誤會引入潛在的耦合,使得原始錯誤對呼叫者可見,增加了程式碼的複雜性,就像在信封上貼了太多標籤,難以辨認信件內容。
可能的影響:
包裝錯誤可能導致呼叫者對錯誤的理解加深,但如果濫用,可能使得錯誤鏈變得混亂,難以追蹤真實錯誤源頭。此外,過度包裝錯誤會增加程式碼的複雜性,影響效能和可讀性。
最佳實踐:
只在需要新增上下文資訊時進行錯誤包裝。避免無意義或重複地包裝錯誤,保持錯誤鏈的清晰和簡潔。使用 fmt.Errorf
搭配 %w
進行包裝時,確保包裝的意義明確且有助於錯誤定位。
改進後的程式碼:
在需要提供更多上下文時進行包裝:
package main
import (
"fmt"
)
func ReadFunTesterFile(filename string) error {
// 模擬讀取檔案錯誤
return fmt.Errorf("不可恢復的錯誤")
}
func OpenFunTesterFile(filename string) error {
err := ReadFunTesterFile(filename)
if err != nil {
return fmt.Errorf("FunTester: 處理檔案 %s 時出錯: %w", filename, err)
}
return nil
}
func main() {
err := OpenFunTesterFile("FunTester.txt")
if err != nil {
fmt.Printf("FunTester: 發生錯誤: %v\n", err)
// 不進一步包裝,保持錯誤鏈清晰
// err = fmt.Errorf("FunTester: 處理檔案時出錯: %w", err)
}
fmt.Println("FunTester: 程式結束")
}
輸出結果:
FunTester: 發生錯誤: FunTester: 處理檔案 FunTester.txt 時出錯: 不可恢復的錯誤
FunTester: 程式結束
錯誤五十:不正確的錯誤型別比較 (#50)
示例程式碼:
package main
import (
"errors"
"fmt"
)
var ErrFunTesterNotFound = errors.New("FunTester: 未找到")
func GetFunTester(id int) error {
if id != 1 {
return fmt.Errorf("FunTester: 獲取FunTester失敗: %w", ErrFunTesterNotFound)
}
return nil
}
func main() {
err := GetFunTester(2)
if err != nil {
// 錯誤比較不正確
if err == ErrFunTesterNotFound {
fmt.Println("FunTester: FunTester未找到")
} else {
fmt.Println("FunTester: 其他錯誤", err)
}
}
}
錯誤說明:
在 Go 1.13 及以上版本,使用 fmt.Errorf
搭配 %w
進行錯誤包裝時,直接使用 ==
運算子比較無法正確判斷錯誤是否是特定型別。需要使用 errors.Is
或 errors.As
來進行比較。這種誤用就像試圖用放大鏡看顯微鏡裡的細節,自然無效。
可能的影響:
錯誤比較不正確會導致錯誤處理邏輯失效,可能無法正確識別和響應特定錯誤型別。這會導致程式無法按照預期處理錯誤,影響程式的穩定性和可靠性。
最佳實踐:
在進行錯誤比較時,使用 errors.Is
或 errors.As
來判斷包裝後的錯誤是否為特定錯誤型別。這確保了錯誤比較的正確性和健壯性,特別是在處理巢狀或包裝錯誤時。
改進後的程式碼:
使用 errors.Is
進行正確的錯誤比較:
package main
import (
"errors"
"fmt"
)
var ErrFunTesterNotFound = errors.New("FunTester: 未找到")
func GetFunTester(id int) error {
if id != 1 {
return fmt.Errorf("FunTester: 獲取FunTester失敗: %w", ErrFunTesterNotFound)
}
return nil
}
func main() {
err := GetFunTester(2)
if err != nil {
// 使用 errors.Is 進行正確的錯誤比較
if errors.Is(err, ErrFunTesterNotFound) {
fmt.Println("FunTester: FunTester未找到")
} else {
fmt.Println("FunTester: 其他錯誤", err)
}
}
}
輸出結果:
FunTester: 其他錯誤 FunTester: 獲取FunTester失敗: FunTester: 未找到
錯誤五十一:不正確的錯誤物件值比較 (#51)
示例程式碼:
package main
import (
"errors"
"fmt"
)
var ErrFunTesterNotFound = errors.New("FunTester: 未找到")
func GetFunTester(id int) error {
if id != 1 {
return fmt.Errorf("FunTester: 獲取FunTester失敗: %w", ErrFunTesterNotFound)
}
return nil
}
func main() {
err := GetFunTester(2)
if err != nil {
// 錯誤物件值比較不正確
if err.Error() == "FunTester: 未找到" {
fmt.Println("FunTester: FunTester未找到")
} else {
fmt.Println("FunTester: 其他錯誤", err)
}
}
}
錯誤說明:
即使錯誤資訊與預期相符,直接透過 err.Error() == "FunTester: 未找到"
進行比較也是不正確的。這不僅效率低下,還容易因為錯誤資訊的微小變化而導致比較失敗。就像是用拼音代替漢字來比較,既不準確又不高效。
可能的影響:
錯誤物件值比較不正確,會導致錯誤處理邏輯無法正確判斷特定錯誤型別,進而影響程式的穩定性和正確性。這可能會導致處理某些錯誤時,無法執行正確的響應措施。
最佳實踐:
始終使用 errors.Is
或 errors.As
進行錯誤比較,避免透過字串比較錯誤資訊。這樣不僅更準確,還能保持程式碼的健壯性和可維護性。
改進後的程式碼:
使用 errors.Is
進行正確的錯誤比較,避免透過字串進行比較:
package main
import (
"errors"
"fmt"
)
var ErrFunTesterNotFound = errors.New("FunTester: 未找到")
func GetFunTester(id int) error {
if id != 1 {
return fmt.Errorf("FunTester: 獲取FunTester失敗: %w", ErrFunTesterNotFound)
}
return nil
}
func main() {
err := GetFunTester(2)
if err != nil {
// 使用 errors.Is 進行正確的錯誤比較
if errors.Is(err, ErrFunTesterNotFound) {
fmt.Println("FunTester: FunTester未找到")
} else {
fmt.Println("FunTester: 其他錯誤", err)
}
}
}
輸出結果:
FunTester: 其他錯誤 FunTester: 獲取FunTester失敗: FunTester: 未找到
錯誤五十二:兩次處理同一個錯誤 (#52)
示例程式碼:
package main
import (
"fmt"
)
func processFunTester() error {
return fmt.Errorf("FunTester: 發生錯誤")
}
func main() {
err := processFunTester()
if err != nil {
fmt.Println("FunTester: 錯誤:", err)
// 再次處理同一個錯誤
fmt.Println("FunTester: 再次處理錯誤:", err)
}
}
錯誤說明:
在 Go 語言中,錯誤在函式內部處理後,可能會被重複處理,如列印日誌和再次返回。兩次處理同一個錯誤會導致日誌冗餘,增加維護難度,甚至混淆錯誤來源,就像同一個問題被反覆提及,卻沒有解決方案。
可能的影響:
兩次處理同一個錯誤會導致日誌中出現重複的錯誤資訊,混淆問題的實際來源,增加除錯難度。此外,重複處理錯誤可能會干擾正常的錯誤處理流程,導致錯誤響應不一致或不完整。
最佳實踐:
在處理錯誤時,應明確責任,決定由函式內處理錯誤還是傳遞給呼叫方處理。避免在函式內部同時列印錯誤日誌和返回錯誤給呼叫方,讓錯誤的處理邏輯清晰且不重複。包裝錯誤時,只提供額外的上下文資訊,而不進行實際的處理。
改進後的程式碼:
選擇由呼叫方負責處理錯誤,函式內僅返回錯誤:
package main
import (
"fmt"
)
func processFunTester() error {
return fmt.Errorf("FunTester: 發生錯誤")
}
func main() {
err := processFunTester()
if err != nil {
// 僅由呼叫方處理錯誤
fmt.Println("FunTester: 錯誤:", err)
// 呼叫方決定是否進一步處理
}
}
如果需要在呼叫方進一步處理,可以傳遞或記錄錯誤,而不重複列印:
package main
import (
"fmt"
)
func processFunTester() error {
return fmt.Errorf("FunTester: 發生錯誤")
}
func main() {
err := processFunTester()
if err != nil {
// 記錄錯誤日誌
fmt.Println("FunTester: 錯誤:", err)
// 再次處理錯誤,例如返回或上報
// fmt.Println("FunTester: 再次處理錯誤:", err) // 避免重複處理
}
}
輸出結果:
FunTester: 錯誤: FunTester: 發生錯誤
錯誤五十三:不處理錯誤 (#53)
示例程式碼:
package main
import (
"fmt"
"os"
)
func main() {
// 忽略讀取檔案時的錯誤
data, _ := os.ReadFile("FunTester.txt")
fmt.Println("FunTester: 檔案內容 =", string(data))
}
錯誤說明:
在 Go 語言中,忽略錯誤處理是一個常見的錯誤,尤其是在函式呼叫或 defer
語句執行時。未能處理錯誤可能導致程式忽略關鍵的問題,繼續執行不安全的邏輯,進而引發更嚴重的問題。就像在嘗試修理機器時,沒有檢查是否安全,可能導致機器損壞甚至人身危險。
可能的影響:
不處理錯誤會導致程式在遇到問題時無法及時響應和修復,導致資料錯誤、資源洩露或程式崩潰。特別是在關鍵操作(如檔案讀取、網路通訊等)中,忽略錯誤會導致嚴重的後果,影響程式的穩定性和可靠性。
最佳實踐:
每次呼叫可能返回錯誤的函式時,都要檢查並適當處理錯誤。即使當前不需要對錯誤進行特別處理,也應至少記錄錯誤日誌,以便後續調查和修復。此外,在 defer
語句中執行的函式,如果返回錯誤,也應處理或記錄,避免錯過關鍵問題。
改進後的程式碼:
顯式處理錯誤,確保程式在錯誤發生時能夠正確響應:
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("FunTester.txt")
if err != nil {
fmt.Printf("FunTester: 讀取檔案失敗: %v\n", err)
return
}
fmt.Println("FunTester: 檔案內容 =", string(data))
}
或者在 defer
函式中處理錯誤:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: 無法建立檔案")
return
}
defer func() {
if cerr := file.Close(); cerr != nil {
fmt.Printf("FunTester: 關閉檔案時出錯: %v\n", cerr)
}
}()
_, err = file.WriteString("FunTester: 寫入內容")
if err != nil {
fmt.Printf("FunTester: 寫入檔案時出錯: %v\n", err)
return
}
}
輸出結果:
FunTester: 檔案內容 = FunTester演示內容
錯誤五十四:不處理 defer
中的錯誤 (#54)
示例程式碼:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: 無法建立檔案")
return
}
defer file.Close()
_, err = file.WriteString("FunTester: 寫入內容")
if err != nil {
fmt.Println("FunTester: 寫入時發生錯誤")
}
}
錯誤說明:
在 Go 語言中,defer
語句用於在函式退出前執行清理操作,如關閉檔案。然而,defer
函式執行時的錯誤往往被忽略,可能導致資源釋放不完全或未記錄的重要錯誤。就像是在清理家務時,不檢查是否所有東西都已整理乾淨,可能留下隱患。
可能的影響:
未處理 defer
中執行的錯誤,會導致資源洩漏(如未關閉的檔案、未釋放的鎖等),影響程式的穩定性和資源管理。此外,遺漏的錯誤資訊會增加除錯難度,導致潛在的問題被忽略。
最佳實踐:
在 defer
中執行的函式,應該檢查並處理返回的錯誤。可以使用匿名函式(閉包)來捕獲並處理錯誤,或者在 defer
內部記錄錯誤日誌,確保不會遺漏重要的錯誤資訊。
改進後的程式碼:
使用匿名函式在 defer
中捕獲和處理錯誤:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: 無法建立檔案")
return
}
defer func() {
if cerr := file.Close(); cerr != nil {
fmt.Printf("FunTester: 關閉檔案時出錯: %v\n", cerr)
}
}()
_, err = file.WriteString("FunTester: 寫入內容")
if err != nil {
fmt.Printf("FunTester: 寫入時發生錯誤: %v\n", err)
}
}
或者記錄錯誤:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: 無法建立檔案")
return
}
defer func() {
if cerr := file.Close(); cerr != nil {
fmt.Printf("FunTester: 關閉檔案時出錯: %v\n", cerr)
}
}()
_, err = file.WriteString("FunTester: 寫入內容")
if err != nil {
fmt.Printf("FunTester: 寫入時發生錯誤: %v\n", err)
}
}
輸出結果檔案 FunTester_output.txt
內容:
FunTester: 寫入內容
說明:
透過在 defer
中使用匿名函式,可以捕獲並處理檔案關閉時可能發生的錯誤,確保資源得以正確釋放,並且錯誤資訊不會被忽略。
FunTester 原創精華
【免費合集】從 Java 開始效能測試
故障測試與 Web 前端
服務端功能測試
效能測試專題
Java、Groovy、Go
白盒、工具、爬蟲、UI 自動化
理論、感悟、影片