Go 語言常見錯誤——異常處理

FunTester發表於2025-03-19

在 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.Iserrors.As 來進行比較。這種誤用就像試圖用放大鏡看顯微鏡裡的細節,自然無效。

可能的影響:
錯誤比較不正確會導致錯誤處理邏輯失效,可能無法正確識別和響應特定錯誤型別。這會導致程式無法按照預期處理錯誤,影響程式的穩定性和可靠性。

最佳實踐:
在進行錯誤比較時,使用 errors.Iserrors.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.Iserrors.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 自動化
理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章