iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
Modern Web

Go 快 Go 高效: 從基礎語法到現代Web應用開發系列 第 21

【Day21】Go 中的錯誤處理 | Error Handling 實踐指南

  • 分享至 

  • xImage
  •  

前言

在軟體開發過程中,我們無法保證程式碼能夠 100% 正常運作。因此,適當的錯誤處理變得至關重要,以防止意外發生並提升應用程式的穩定性與可靠性。本篇文章將帶領你了解 Go 語言中的錯誤處理機制,並介紹如何自建一套易用的錯誤處理模式。


Panic

panic 是 Go 語言中一種處理不可預期錯誤的機制,類似於其他語言中的異常(Exception)。當發生無法恢復的錯誤時,可以使用 panic 來終止程式的執行。

package main

import (
    "fmt"
)

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    fmt.Println("Result:", divide(10, 2))
    fmt.Println("This line will not be executed if panic occurs.")
    fmt.Println("Result:", divide(10, 0))
}
</* Output: */>
Result: 5
This line will not be executed if panic occurs.
Recovered from panic: division by zero

divide 函數的除數為零時,會觸發 panic
使用 defer 搭配 recover 可以捕捉 panic,避免程式崩潰,並進行相應的處理。


Error

Go 語言的錯誤處理主要依賴於內建的 error 類型。這種方式鼓勵開發者在函數返回值中顯式處理錯誤,提升了程式碼的可讀性與可維護性。

package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero is not allowed")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }

    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}
</* Output: */>
Result: 5
Error: division by zero is not allowed

divide 函數返回一個 int 和一個 error
main 函數中,通過檢查 err 是否為 nil 來決定如何處理結果。


Logger

在實際應用中,除了處理錯誤,記錄錯誤信息也是非常重要的。Go 語言提供了多種方式來進行日誌記錄,例如使用內建的 log 包或第三方日誌庫。(那這裡我以第三方為例)

  • 安裝套件
go get -u github.com/sirupsen/logrus
  • 然後建立一個 util 資料夾,檔案名 logger.go
package logger

import (
	"github.com/sirupsen/logrus"
	"os"
)

var Log *logrus.Logger

func init() {
	Log = logrus.New()

	file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err == nil {
		Log.Out = file
	} else {
		logrus.Fatal(err)
	}

	Log.SetOutput(os.Stdout)

	Log.SetFormatter(&logrus.TextFormatter{
		FullTimestamp: true,
	})
	Log.SetLevel(logrus.InfoLevel)
}
  • 在我們的主程式
package main

import (
	"demo/logger"
	"errors"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero is not allowed")
	}
	return a / b, nil
}

func main() {

	result, err := divide(10, 0)
	if err != nil {
		util.Log.Error(err)
	}
	util.Log.Info(result)
}
</* Output: */>
ERRO[2024-09-29T13:45:26+08:00] division by zero is not allowed              
INFO[2024-09-29T13:45:26+08:00] 0   

可以看到現在對於debug區域中,我們可以更好的來查看當前遇到的狀況是什麼,那除了Error和info以外,還有以下幾種選項。

// Debug: 適用於開發過程中詳細的日誌紀錄。
Log.Debug("This is a debug message")

// Info: 用於一般信息,表示 app 正常運作的信息。
Log.Info("This is an info message")

// Warn: 用於警告信息,表示可能會導致問題的情况。
Log.Warn("This is a warning message")

// Error: 用於錯誤信息,表示程序發生了錯誤,但未導致程序停止運行。
Log.Error("This is an error message")

// Fatal: 用於嚴重錯誤信息,表示程序將停止運行。調用該方法後會自動調用 os.Exit(1) 退出程序。
Log.Fatal("This is a fatal message")

// Panic: 用於非常嚴重的錯誤信息,觸發 panic 導致程序崩潰。調用該方法後會引發 panic。
Log.Panic("This is a panic message")

比較

特性 panic error Logger
說明 用於處理不可恢復的錯誤,會終止程式的執行。 用於處理可預期的錯誤,通過返回值讓調用者處理。 用於記錄錯誤、資訊、警告等運行時訊息。
使用情境 嚴重錯誤或不可預期的情況,如系統資源耗盡或數據損壞。 日常錯誤處理,如函數參數錯誤、文件讀取失敗等。 記錄應用程式的運行狀況、錯誤信息、調試訊息等。
行為 觸發 panic 後,程式會中止執行,除非使用 recover 捕捉。 返回錯誤值,調用者需要檢查並處理錯誤。 不會改變程式的執行流程,只是將訊息輸出到日誌中。
優點 簡單直接,能夠迅速終止錯誤狀況,適用於無法恢復的錯誤。 提供清晰的錯誤處理流程,易於追踪和管理。 提供詳細的運行時資訊,便於調試和監控應用程式。
缺點 可能導致整個應用程式崩潰,過度使用會降低程式穩定性。 需要在每個可能出錯的地方進行錯誤檢查,可能增加代碼複雜度。 增加代碼的複雜性和維護成本,可能影響性能(尤其是在高頻率記錄時)。
處理方式 會中止當前程式的執行,除非在 defer 函數中使用 recover 捕捉。 函數返回 error 類型,調用者需檢查並處理。 使用內建的 log 包或第三方日誌庫(如 logrus)來記錄訊息。
建議 僅在極端情況下使用,不應作為日常錯誤處理手段。 廣泛應用於需要錯誤處理的函數,保持程式碼的穩定性和可維護性。 結合 error 使用,記錄錯誤和關鍵運行信息,提升應用程式的可觀察性。

自建一套易用的 Error Handling 模式

為了提升錯誤處理的靈活性與可維護性,我們可以自建一套錯誤處理模式。本文將介紹如何在 Go 語言中實現 Result Pattern,這是一種容錯的編程模式,能夠處理不同的結果狀態,如成功、失敗或異常。

  • 定義 Result 結構
    首先,創建一個表示操作結果狀態的枚舉:
package main

import (
    "fmt"
    "errors"
)

type ResultState int

const (
    Success ResultState = iota
    Failure
)
  • 定義一個泛型的 Result 結構,包含狀態、值和錯誤:
type Result[T any, E error] struct {
    state  ResultState
    value  T
    error  E
}

// Getter methods
func (r *Result[T, E]) GetState() ResultState {
    return r.state
}

func (r *Result[T, E]) GetValue() T {
    return r.value
}

func (r *Result[T, E]) GetError() E {
    return r.error
}

// Setter methods
func (r *Result[T, E]) SetValue(value T) {
    r.state = Success
    r.value = value
}

func (r *Result[T, E]) SetError(err E) {
    r.state = Failure
    r.error = err
    // 可在此處擴展錯誤處理,例如記錄日誌
}
  • 泛型使用:Result 結構使用泛型 TE,允許處理不同類型的返回值和錯誤,提升了結構的通用性。
  • 狀態管理:透過 ResultState 列舉來表示操作的結果狀態,方便快速判斷是成功還是失敗。
  • 擴展性:在 SetError 方法中,可以添加額外的錯誤處理邏輯,如將錯誤記錄到應用程式日誌中。
  • 使用自訂錯誤類型

為了更好地管理錯誤,我們可以定義自訂的錯誤類型。例如,創建一個名為 DomainError 的結構體,包含自訂的錯誤類型和實際的錯誤訊息:

type DomainErrorType int

const (
    DatabaseError DomainErrorType = iota
    NetworkError
    // 其他錯誤類型
)

type DomainError struct {
    Type    DomainErrorType
    Message string
}

func (e DomainError) Error() string {
    return fmt.Sprintf("🚨: %s", e.Message)
}
  • DomainErrorType 枚舉定義了不同的錯誤類型。
  • DomainError 結構體包含錯誤類型和錯誤訊息,並實現了 Error 方法,使其符合 error 接口。
  • 應用範例
// 成功回應
func GetGreeting() Result[string, error] {
	var result Result[string, error]
	result.SetValue("👍:well down, my son!")
	return result
}

// 自訂錯誤回應
func GetDatabaseStatus() Result[string, DomainError] {
	var result Result[string, DomainError]
	result.SetError(DomainError{
		Type:    DatabaseError,
		Message: "Database service unavailable",
	})
	return result
}

// Go 錯誤回應
func PerformOperation() Result[string, error] {
	var result Result[string, error]
	result.SetError(errors.New("🚨: This is an error using the Go `error` type"))
	return result
}

func main() {
	// 成功範例
	greetingResult := GetGreeting()
	if greetingResult.GetState() == Success {
		fmt.Println(greetingResult.GetValue())
	} else {
		fmt.Println("Error:", greetingResult.GetError())
	}

	// 自訂錯誤範例
	dbStatusResult := GetDatabaseStatus()
	if dbStatusResult.GetState() == Success {
		fmt.Println(dbStatusResult.GetValue())
	} else {
		fmt.Println(dbStatusResult.GetError())
	}

	// Go 錯誤範例
	operationResult := PerformOperation()
	if operationResult.GetState() == Success {
		fmt.Println(operationResult.GetValue())
	} else {
		fmt.Println(operationResult.GetError())
	}
}
</* Output: */>
👍:well down, my son!
🚨: Database service unavailable
This is an error using the Go `error` type
  • GetGreeting 函數返回一個成功的 Result,包含問候語。
  • GetDatabaseStatus 函數返回一個失敗的 Result,使用自訂的 DomainError
  • PerformOperation 函數返回一個失敗的 Result,使用 Go 原生的 error 類型。
  • main 函數中,根據 Result 的狀態進行相應的處理。

總結

在 Go 語言中,錯誤處理是一個不可或缺的部分。透過內建的 error 類型和多重返回值,開發者可以有效地管理和處理錯誤。然而,為了進一步提升錯誤處理的靈活性和可維護性,自建一套如 Result Pattern 的錯誤處理模式是非常有價值的。這不僅能夠讓錯誤處理更加結構化,還能夠輕鬆擴展以滿足應用程式的需求。


參考來源


上一篇
【Day20】RESTful API 設計 II | 寫入方式 (Create、Updata、Delete) 介紹
系列文
Go 快 Go 高效: 從基礎語法到現代Web應用開發21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言