Go項(xiàng)目Error的統(tǒng)一規(guī)劃管理和處理策略
這一篇文章我們來(lái)探討一下怎么在項(xiàng)目初期提前規(guī)劃,把項(xiàng)目的各種Error統(tǒng)一管理起來(lái),以及寫代碼遇到Error時(shí)在不同的代碼層我們應(yīng)該怎么處理它們。
圖片
怎么把項(xiàng)目的Error管理起來(lái)
在聊怎么把Error管理起來(lái)之前我們先來(lái)聊一下為什么要管理Error,有Error就有Error唄,把信息返回給調(diào)用者不就行了?這里說(shuō)的調(diào)用者指的是請(qǐng)求我們系統(tǒng)API的調(diào)用方。
乍一想好像確實(shí)沒(méi)毛病,但是咱們把眼光放到團(tuán)隊(duì)開發(fā)和對(duì)接上。
其一:與咱們對(duì)接的系統(tǒng),判斷錯(cuò)誤一般靠的都是錯(cuò)誤響應(yīng)里的Code碼,如果同一個(gè)類型的錯(cuò)誤你返回不同的錯(cuò)誤碼,一兩個(gè)還好,如果十個(gè)八個(gè)估計(jì)對(duì)方就要過(guò)來(lái)找你們算帳了。
其二:既然一類錯(cuò)誤的錯(cuò)誤碼要統(tǒng)一,那每次都自己NewError再設(shè)置它的錯(cuò)誤碼,這樣即使只有一個(gè)人開發(fā)這個(gè)項(xiàng)目,次數(shù)多了也會(huì)把錯(cuò)誤碼寫錯(cuò)的,更別提多個(gè)人一起開發(fā)的時(shí)候了。
所以就需要我們先把常見的能想到的錯(cuò)誤預(yù)先定義出來(lái)。這也就是為什么咱們的項(xiàng)目在error模塊的code.go中預(yù)先定義了下面幾個(gè)基礎(chǔ)的錯(cuò)誤。
var (
Success = newError(0, "success")
ErrServer = newError(10000000, "服務(wù)器內(nèi)部錯(cuò)誤")
ErrParams = newError(10000001, "參數(shù)錯(cuò)誤, 請(qǐng)檢查")
ErrNotFound = newError(10000002, "資源未找到")
ErrPanic = newError(10000003, "(*^__^*)系統(tǒng)開小差了,請(qǐng)稍后重試") // 無(wú)預(yù)期的panic錯(cuò)誤
ErrToken = newError(10000004, "Token無(wú)效")
ErrForbidden = newError(10000005, "未授權(quán)") // 訪問(wèn)一些未授權(quán)的資源時(shí)的錯(cuò)誤
ErrTooManyRequests = newError(10000006, "請(qǐng)求過(guò)多")
ErrCoverData = newError(10000007, "ConvertDataError") // 數(shù)據(jù)轉(zhuǎn)換錯(cuò)誤
)
除了這些通用的錯(cuò)誤之外,我們可以預(yù)先按照項(xiàng)目的模塊分配每個(gè)業(yè)務(wù)模塊錯(cuò)誤的碼段。比如在未來(lái)的項(xiàng)目代碼中你會(huì)看到一些給業(yè)務(wù)模塊單獨(dú)定義的錯(cuò)誤碼。
// 用戶模塊相關(guān)錯(cuò)誤碼 10000100 ~ 1000199
var (
ErrUserInvalid = newError(10000101, "用戶異常")
ErrUserNameOccupied = newError(10000102, "用戶名已被占用")
...
)
// 商品模塊相關(guān)錯(cuò)誤碼 10000200 ~ 1000299
var (
ErrCommodityNotExists = newError(10000200, "商品不存在")
ErrCommodityStockOut = newError(10000201, "庫(kù)存不足")
...
)
// 購(gòu)物車模塊相關(guān)錯(cuò)誤碼 10000300 ~ 1000399
var (
ErrCartItemParam = newError(10000300, "購(gòu)物項(xiàng)參數(shù)異常")
ErrCartWrongUser = newError(10000301, "用戶購(gòu)物信息不匹配")
...
)
到這里一直說(shuō)的都是預(yù)先定義錯(cuò)誤,那針對(duì)一些不知道什么類型的錯(cuò)誤該怎么辦?比如在DAO層做了一下CRUD出現(xiàn)了Error,難道還要預(yù)先定義一個(gè)ErrDBQuery 之類的錯(cuò)誤嗎?那項(xiàng)目用的中間件多了,Redis、MQ什么的都要預(yù)先定義錯(cuò)誤嗎?
這里我給我的方案是,調(diào)用其他外部基礎(chǔ)組件出錯(cuò)時(shí),調(diào)用一個(gè)SDK方法出錯(cuò)時(shí),把底層錯(cuò)誤包裝成項(xiàng)目的Error。
func Wrap(msg string, err error) *AppError {
if err == nil {
return nil
}
appErr := &AppError{code: -1, msg: msg, cause: err}
return appErr
}
當(dāng)你拿到一個(gè)error不確定它該是什么錯(cuò)誤,你就用這個(gè)Wrap方法包裝成項(xiàng)目的App Error。
下一節(jié)我們封裝的統(tǒng)一接口響應(yīng)組件會(huì)使用下面的方法來(lái)獲取Error對(duì)應(yīng)的HTTP Code。
func (e *AppError) HttpStatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ErrParams.Code():
return http.StatusBadRequest
case ErrNotFound.Code():
......
default:
return http.StatusInternalServerError
}
}
不同代碼層該怎么處理Error
我們?cè)趯懘a的時(shí)候?yàn)榱吮kU(xiǎn),都愛(ài)在 error 判斷中打一條Error級(jí)別的日志,這樣好歹遇到錯(cuò)誤了在日志中會(huì)留下痕跡,到了真需要排除問(wèn)題的時(shí)候總比那些什么日志都不記錄的寫法要好多了。
很多時(shí)候我們遇到線上問(wèn)題了,查半天最后實(shí)現(xiàn)沒(méi)辦法就加幾條日志部署上去觀察觀察情況,等同樣的錯(cuò)誤發(fā)生了再去看新打的日志。
但是不知道大家有沒(méi)有發(fā)現(xiàn),如果你每遇到Error都打一條日志的話,那么這個(gè)錯(cuò)誤信息在日志里的重復(fù)率時(shí)相當(dāng)?shù)母?,發(fā)生了一個(gè)錯(cuò)誤,好幾條日志都是這個(gè)錯(cuò)誤信息,其實(shí)都是同一個(gè)錯(cuò)誤,只不過(guò)這些日志是在調(diào)用邏輯的不同代碼層做被打進(jìn)去的。
那么關(guān)于什么錯(cuò)誤該記日志,什么不該記,有沒(méi)有什么好用的標(biāo)準(zhǔn)?不好意思沒(méi)有,全靠自己的悟性。。。。。。聽到這里是不是想罵人了。
這里分享一下國(guó)外論壇中經(jīng)??吹降?nbsp;Only handle errors once的原則
Go程序錯(cuò)誤處理的原則:
- 程序底層 (Dao、基礎(chǔ)設(shè)施層) 拋出錯(cuò)誤
- 程序中層(領(lǐng)域服務(wù)層、應(yīng)用服務(wù)層)包裝錯(cuò)誤
- 程序上層(控制層) 記錄錯(cuò)誤
如果每一層都打日志,查詢?nèi)罩镜臅r(shí)候必然會(huì)有不少重復(fù),當(dāng)然這個(gè)見仁見智,多打點(diǎn)日志也沒(méi)錯(cuò)總比不打日志,出問(wèn)題了再打日志,等線上復(fù)現(xiàn)問(wèn)題后再排查日志要強(qiáng)多了。
還有一個(gè)原因就是Go的原生 Error 如果你不自己做自定義封裝確實(shí)能給咱們的有效信息很少,我們看到錯(cuò)誤信息經(jīng)常是找半天才能找到原因。