iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Software Development

Go Clean Architecture API 開發全攻略系列 第 21

[Day 21] 優雅地處理錯誤(二):定義自己的錯誤類型

  • 分享至 

  • xImage
  •  

在上一篇文章中,我們介紹了如何在六邊形架構中優雅地處理錯誤,並建立了一套從資料庫層到 HTTP 回應的完整錯誤處理流程。
這一篇,我們將進一步探討如何定義自己的錯誤類型,以提升錯誤處理的靈活性和表達力。

為什麼要定義自己的錯誤類型?

在 Go 語言中,錯誤是一個實作了 error 介面的物件。雖然我們可以使用內建的 error 類型來表示錯誤,但這種方式有一些限制:

  1. 缺乏結構化資訊:內建的 error 類型只能提供一個錯誤訊息字串,無法攜帶更多的上下文資訊。
  2. 難以分類:當我們需要根據錯誤的類型來決定處理方式時,使用字串比較會變得困難且容易出錯。
  3. 不易擴展:隨著應用的成長,我們可能需要為錯誤添加更多的欄位或方法,這在使用內建的 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)。
這樣的設計讓我們可以在錯誤中攜帶更多的結構化資訊,並且更容易分類和擴展。

在各層中使用自訂錯誤類型

在各層中使用自訂錯誤類型,可以讓我們更清晰地表達錯誤的意圖,並且更容易地進行錯誤處理。

  • Repository 層:在 Repository 層中,我們可以根據資料庫操作的結果,回傳對應的 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()
}
  • Service 層:在 Service 層中,我們可以根據外部服務的結果,回傳對應的 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
}
  • Usecase 層:在 Usecase 層中,我們可以根據業務邏輯的結果,回傳對應的 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)
	}
    // ...
}
  • Controller 層:在上一篇中, Controller 層已經將 Error Pass 給 Middleware 處理了。
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
			}
			...
    }
}

總結

通過定義自己的錯誤類型,我們提升了錯誤處理的靈活性和表達力。
我們可以在錯誤中攜帶更多的結構化資訊,並且更容易分類和擴展。
這讓我們的應用程式在面對錯誤時,能夠更清晰地表達意圖,並且更容易地進行錯誤處理。


上一篇
[Day 20] 優雅地處理錯誤(一):設計統一的 API 錯誤回報機制
下一篇
[Day 22] Go 單元測試:如何 Mock 資料庫與外部依賴
系列文
Go Clean Architecture API 開發全攻略22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言