Go 項目實戰 - 讓自定義 Error 支持 Go 的 errors-Is 判定以及原型模式的應用

經過前面三節高代碼強度的學習,相信大家都已經有點累了,本節我們不着急繼續 “趕路”,休息片刻!我們換個輕鬆點的話題,聊一聊咱們項目定製化 Error--AppError 怎麼支持 Go 語言的 errors.Is 判定,以及項目預定義的那些 Error 在實際使用過程中某些情況下會出現循環引用的問題,我們會利用一個原型設計模式來解決這個問題。

項目定製化 Error 回顧

定義項目 Error 實現錯誤鏈和發生位置記錄這篇文章中我們給項目定義了自己的 Error 類型 AppError

type AppError struct {
 code  int    `json:"code"`
 msg   string `json:"msg"`
 cause error  `json:"cause"`
 occurred string `json:"occurred"`
}

目的是爲了通過自己的封裝讓 Go 的 Error 能支持錯誤原因和發生位置的記錄。同時我們還爲項目的開發預定義了很多 Error 變量。

// 用戶模塊相關錯誤碼 10000100 ~ 1000199
var (
 ErrUserInvalid      = newError(10000101, "用戶異常")
 ErrUserNameOccupied = newError(10000102, "用戶名已被佔用")
 ErrUserNotRight     = newError(10000103, "用戶名或密碼不正確")
)
// 商品模塊相關錯誤碼 10000200 ~ 1000299
var (
 ErrCommodityNotExists = newError(10000200, "商品不存在")
 ErrCommodityStockOut  = newError(10000201, "庫存不足")
)
// 購物車模塊相關錯誤碼 10000300 ~ 1000399
var (
 ErrCartItemParam = newError(10000300, "購物項參數異常")
 ErrCartWrongUser = newError(10000301, "用戶購物信息不匹配")
)

在 Go 項目 Error 的統一管理和處理建議 中我建議需要返回給客戶端返回約定好的錯誤碼的錯誤都在這裏預先定義。而對於一些像 DB、存儲之類錯誤產生的 Error 無需告知客戶端明確原因只需要讓客戶端發生了服務端內部錯誤的情況、或者不知道怎麼處理的底層錯誤,我們先統一用 Wrap 方法把它包裝成應用的 AppError。

err = DBMaster().WithContext(ud.ctx).Create(userModel).Error
if err != nil {
    err = errcode.Wrap("UserDaoCreateUserError", err)
    return nil, err
}

在設計的過程中,覺得已經夠全面了,但是隻要真正把它來開發需求時,還是能發現有問題的,具體什麼問題呢? 我們繼續往下看。

怎麼讓自定義 Error 支持 Go 的 errors.Is 判定

想讓 Error 支持 Go 的 errors.Is 判定,我們先來看看 errors.Is 方法裏到底是怎麼判定 Error 是不是給定的目標錯誤的,其源碼如下:

func Is(err, target error) bool {
 if target == nil {
  return err == target
 }

 isComparable := reflectlite.TypeOf(target).Comparable()
 for {
  if isComparable && err == target {
   return true
  }
  if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
   return true
  }
  if err = Unwrap(err); err == nil {
   return false
  }
 }
}

其源碼中邏輯是會一層層地解包 error,每層的 error 都去看看是否已經實現了 interface{Is(error) bool } 這個接口,如果實現了調用該層 error 的 Is 方法進行判定,如果跟給定 error 不相等或者沒有實現 Is 接口,則繼續用 Uwrap 解包,解到不能解爲止 (err == nil)。

所以這裏給我們預留了兩個接口,我們讓 AppError 實現 Is 和 Uwrap 方法後,它也就支持 Go 的 errors.Is 判定啦。

下面我們在 common/errcode/error.go 中加入這兩個方法的實現。

package errcode

func (e *AppError) UnWrap() error {
 return e.cause
}

// Is 與上面的UnWrap一起讓 *AppError 支持 errors.Is(err, target)
func (e *AppError) Is(target error) bool {
 targetErr, ok := target.(*AppError)
 if !ok {
  return false
 }
 return targetErr.Code() == e.Code()
}

關於項目自定義 Error 的優化,在課程中我還使用了這裏使用設計模式裏的原型模式, 把項目預定義的全局錯誤都是當作原型 - prototype,保證我們既能規範管理我們項目的錯誤碼,也能更自由放心地在程序中使用它們。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PM2UpcZswxN72Q8_fc2oCg