在軟體開發過程中,我們無法保證程式碼能夠 100% 正常運作。因此,適當的錯誤處理變得至關重要,以防止意外發生並提升應用程式的穩定性與可靠性。本篇文章將帶領你了解 Go 語言中的錯誤處理機制,並介紹如何自建一套易用的錯誤處理模式。
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
,避免程式崩潰,並進行相應的處理。
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
來決定如何處理結果。
在實際應用中,除了處理錯誤,記錄錯誤信息也是非常重要的。Go 語言提供了多種方式來進行日誌記錄,例如使用內建的 log 包或第三方日誌庫。(那這裡我以第三方為例)
go get -u github.com/sirupsen/logrus
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 使用,記錄錯誤和關鍵運行信息,提升應用程式的可觀察性。 |
為了提升錯誤處理的靈活性與可維護性,我們可以自建一套錯誤處理模式。本文將介紹如何在 Go 語言中實現 Result Pattern,這是一種容錯的編程模式,能夠處理不同的結果狀態,如成功、失敗或異常。
package main
import (
"fmt"
"errors"
)
type ResultState int
const (
Success ResultState = iota
Failure
)
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
結構使用泛型T
和E
,允許處理不同類型的返回值和錯誤,提升了結構的通用性。- 狀態管理:透過
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
的錯誤處理模式是非常有價值的。這不僅能夠讓錯誤處理更加結構化,還能夠輕鬆擴展以滿足應用程式的需求。