學(xué)會定制化 Go 項目的 error,回溯錯誤的原因和發(fā)生位置
Go語言的Error處理一直被人吐槽,吐槽的點(diǎn)除了一個接一個的 if err != nil 的判斷外,還有人說Go的錯誤太原始不能像其他語言那樣在拋出異常的時候的時候傳一個Casue Exception 把導(dǎo)致異常的整個原因鏈串起來。
第一點(diǎn)確實(shí)是事實(shí),但是寫習(xí)慣了也能接受,而且對新手友好。第二點(diǎn)屬實(shí)就有點(diǎn)尬黑了。
用Go開發(fā)項目時想讓程序拋出的 error 信息不要那么單薄,需要自己搭建項目時先做一番基礎(chǔ)工作,自己定義項目的Error類型在包裝錯誤的時候記錄上錯誤的原因和發(fā)生的位置,比如像下面這樣。
{
"code": 10000000,
"msg": "服務(wù)器內(nèi)部錯誤",
"cause": "db error: undefined column user_id",
"occurred": "go-study-lab/go-mall.TestAppError, file: building.go, line: 69"
}
同時它還要實(shí)現(xiàn)Go的error interface,能融入Go 的錯誤處理機(jī)制才行。
今天我就帶大家通過自定義項目Error并實(shí)現(xiàn) Go error interface ,讓你的Go項目Error擁有更豐富的錯誤原因和發(fā)生位置的信息??吹揭粋€錯誤能看出來時什么原因?qū)е碌?、以及是哪的代碼導(dǎo)致的這樣能大大降低Go項目的維護(hù)難度。
Go Error 定制化
定義項目的Error結(jié)構(gòu)
首先我們在項目的common目錄中增加errcode目錄,該目錄下會創(chuàng)建兩個文件error.go 和 code.go。error.go文件用來存放自定義Error的結(jié)構(gòu)和相關(guān)方法,code.go 用來放置項目各種預(yù)定義的Error。
.
|-- common
| |-- errcode
| |---code.go
| |---error.go
|-- main.go
|-- go.mod
|-- go.sum
我們現(xiàn)在error.go 中定義AppError
type AppError struct {
code int `json:"code"`
msg string `json:"msg"`
cause error `json:"cause"`
}
cause 字段保存的是導(dǎo)致產(chǎn)生 AppErr 的原因,比如一個數(shù)據(jù)庫查詢語法錯誤,拿它再來生成項目的 AppError 或者是給預(yù)定義好的 AppError 追加上這個原因的error, 這樣就能達(dá)到保存錯誤鏈條的目的。
現(xiàn)在AppError 還不是 error 類型,需要讓他實(shí)現(xiàn)Go的 error interface,這個接口如下。
type error interface {
Error() string
}
其中只定義了一個方法,我們讓AppError實(shí)現(xiàn)Error方法把它變成 error 類型。
func (e *AppError) Error() string {
if e == nil {
return ""
}
formattedErr := struct {
Code int `json:"code"`
Msg string `json:"msg"`
Cause string `json:"cause"`
}{
Code: e.Code(),
Msg: e.Msg(),
}
if e.cause != nil {
formattedErr.Cause = e.cause.Error()
}
errByte, _ := json.Marshal(formattedErr)
return string(errByte)
}
Error方法返回的是AppError對象的JSON序列化字符串,其中如果cause字段不為空即錯誤原因不為空,再去錯誤原因的Error方法拿到底層的錯誤信息。
我們把Stringer 接口也實(shí)現(xiàn)一下,在沒有類型字段轉(zhuǎn)換的地方,它還是*AppErr類型,保證這個時候序列化它的時候仍然能得到期望的信息。
func (e *AppError) String() string {
return e.Error()
}
接下來我們在code.go 中先預(yù)定義一些基礎(chǔ)的錯誤
var (
Success = newError(0, "success")
ErrServer = newError(10000000, "服務(wù)器內(nèi)部錯誤")
ErrParams = newError(10000001, "參數(shù)錯誤, 請檢查")
ErrNotFound = newError(10000002, "資源未找到")
ErrPanic = newError(10000003, "(*^__^*)系統(tǒng)開小差了,請稍后重試") // 無預(yù)期的panic錯誤
ErrToken = newError(10000004, "Token無效")
ErrForbidden = newError(10000005, "未授權(quán)") // 訪問一些未授權(quán)的資源時的錯誤
ErrTooManyRequests = newError(10000006, "請求過多")
)
上面大家看到了 AppError 的類型定義中,字段的訪問性都是包內(nèi)可訪問的,所以我們要定義一些 getter 方法,這樣接口返回錯誤響應(yīng)時,才能讀到錯誤碼和錯誤信息。
func (e *AppError) Code() int {
return e.code
}
func (e *AppError) Msg() string {
return e.msg
}
func (e *AppError) HttpStatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ErrServer.Code(), ErrPanic.Code():
return http.StatusInternalServerError
case ErrParams.Code():
return http.StatusBadRequest
case ErrNotFound.Code():
return http.StatusNotFound
case ErrTooManyRequests.Code():
return http.StatusTooManyRequests
case ErrToken.Code():
return http.StatusUnauthorized
case ErrForbidden.Code():
return http.StatusForbidden
default:
return http.StatusInternalServerError
}
}
這里的 HttpStatusCode 返回的是HTTP 狀態(tài)碼,如果你們的研發(fā)習(xí)慣是請求接口的響應(yīng)一律是 HTTP 200 再通過相應(yīng)里的code碼判斷是否正確,這個方法可以放著不用, 規(guī)范化一點(diǎn)肯定是這種比較好,況且HTTP Status 不是 200 狀態(tài)碼,也是可以返回 code msg 那些信息給客戶端的。
底層Error怎么變成項目Error
上面我們預(yù)定義好了幾個應(yīng)用錯誤,這里說明一下,預(yù)定義好的錯誤會最終返回給發(fā)起請求的客戶端,所以控制器層各個URI的路由處理控制器中最后一定要返回預(yù)定義的錯誤,這個我們會在未來給Go項目封裝統(tǒng)一的響應(yīng)組件時處理。
那一個底層的錯誤怎么才能變成我們自定義的錯誤呢?