在上一篇文章中,我們介紹了如何在六邊形架構中優雅地處理錯誤,並建立了一套從資料庫層到 HTTP 回應的完整錯誤處理流程。
這一篇,我們將進一步探討如何定義自己的錯誤類型,以提升錯誤處理的靈活性和表達力。
在 Go 語言中,錯誤是一個實作了 error 介面的物件。雖然我們可以使用內建的 error 類型來表示錯誤,但這種方式有一些限制:
為了解決這些問題,我們可以定義自己的錯誤類型,讓錯誤能夠攜帶更多的資訊,並且更容易分類和擴展。
定義自己的錯誤類型非常簡單,只需要建立一個結構體,並實作 error 介面即可。
type GPErrorCode int
const (
ErrCodeParametersNotCorrect GPErrorCode = 400000
...
)
func (code GPErrorCode) Message() string {
switch code {
case ErrCodeParametersNotCorrect:
return "Parameters Not Correct"
...
default:
return fmt.Sprintf("Unknown Error Code: %d", code)
}
}
type GPError struct {
code GPErrorCode // 錯誤碼
origin error // 原始錯誤
}
func (e GPError) Error() string {
if e.origin == nil {
return fmt.Sprintf("Code: %d, Message: %s", e.code, e.code.Message())
}
return fmt.Sprintf("Code: %d, Error: %s", e.code, e.origin.Error())
}
func (e *GPError) Append(msg string) *GPError {
if e.origin == nil {
e.origin = errors.New(msg)
}
e.origin = fmt.Errorf("%s: %w", msg, e.origin)
return e
}
func (e *GPError) HttpStatusCode() int {
if int(e.code) >= int(ErrCodeInternalServer) {
return http.StatusInternalServerError
}
return http.StatusBadRequest
}
func (e *GPError) Message() string {
if e.HttpStatusCode() >= http.StatusInternalServerError {
return "Internal Server Error"
}
return e.code.Message()
}
這樣,我們就定義了一個 GPError
類型,它包含了一個錯誤碼(GPErrorCode
)和一個原始錯誤(origin
)。
這樣的設計讓我們可以在錯誤中攜帶更多的結構化資訊,並且更容易分類和擴展。
在各層中使用自訂錯誤類型,可以讓我們更清晰地表達錯誤的意圖,並且更容易地進行錯誤處理。
GPError
。func (d *Database) FindUserByEmail(ctx context.Context, email string) (*domain.DBUserModel, *domain.GPError) {
var user entity.User
err := d.db.WithContext(ctx).
Model(&entity.User{}).
Where("email = ?", email).
First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, domain.NewGPErrorWithError(domain.ErrCodeDatabaseError, err)
}
return user.ToDomain()
}
GPError
。func (s *Service) Compare(hashed, password string) *domain.GPError {
err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
// 轉換為 domain 錯誤,這樣 Usecase 層就不需要知道 bcrypt 的錯誤細節
return domain.NewGPError(domain.ErrCodeInvalidPassword)
}
return domain.NewGPErrorWithError(domain.ErrCodeInternalServer, err).Append("failed to compare password")
}
return nil
}
GPError
。func (u *UseCase) Execute(ctx context.Context, input Input) (*Output, *domain.GPError) {
// 1. 根據 email 從資料庫取得 user 資料
user, err := u.repository.FindUserByEmail(ctx, input.email)
if err != nil {
return nil, err.Append("failed to find user by email")
}
if user == nil {
return nil, domain.NewGPError(domain.ErrCodeUserNotFound)
}
// ...
}
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
...
// 取得最後一個錯誤
err := c.Errors.Last().Err
// 判斷錯誤類型,並回傳適當的 HTTP 狀態碼
if gpErr, ok := err.(domain.GPError); ok {
c.JSON(gpErr.HttpStatusCode(), gin.H{"error": gpErr.Message()})
return
}
...
}
}
通過定義自己的錯誤類型,我們提升了錯誤處理的靈活性和表達力。
我們可以在錯誤中攜帶更多的結構化資訊,並且更容易分類和擴展。
這讓我們的應用程式在面對錯誤時,能夠更清晰地表達意圖,並且更容易地進行錯誤處理。