學(xué)會(huì)定制化 Go 項(xiàng)目的 error,回溯錯(cuò)誤的原因和發(fā)生位置
Go語(yǔ)言的Error處理一直被人吐槽,吐槽的點(diǎn)除了一個(gè)接一個(gè)的 if err != nil 的判斷外,還有人說(shuō)Go的錯(cuò)誤太原始不能像其他語(yǔ)言那樣在拋出異常的時(shí)候的時(shí)候傳一個(gè)Casue Exception 把導(dǎo)致異常的整個(gè)原因鏈串起來(lái)。
第一點(diǎn)確實(shí)是事實(shí),但是寫(xiě)習(xí)慣了也能接受,而且對(duì)新手友好。第二點(diǎn)屬實(shí)就有點(diǎn)尬黑了。
用Go開(kāi)發(fā)項(xiàng)目時(shí)想讓程序拋出的 error 信息不要那么單薄,需要自己搭建項(xiàng)目時(shí)先做一番基礎(chǔ)工作,自己定義項(xiàng)目的Error類型在包裝錯(cuò)誤的時(shí)候記錄上錯(cuò)誤的原因和發(fā)生的位置,比如像下面這樣。
{
"code": 10000000,
"msg": "服務(wù)器內(nèi)部錯(cuò)誤",
"cause": "db error: undefined column user_id",
"occurred": "go-study-lab/go-mall.TestAppError, file: building.go, line: 69"
}
同時(shí)它還要實(shí)現(xiàn)Go的error interface,能融入Go 的錯(cuò)誤處理機(jī)制才行。
今天我就帶大家通過(guò)自定義項(xiàng)目Error并實(shí)現(xiàn) Go error interface ,讓你的Go項(xiàng)目Error擁有更豐富的錯(cuò)誤原因和發(fā)生位置的信息。看到一個(gè)錯(cuò)誤能看出來(lái)時(shí)什么原因?qū)е碌摹⒁约笆悄牡拇a導(dǎo)致的這樣能大大降低Go項(xiàng)目的維護(hù)難度。
Go Error 定制化
定義項(xiàng)目的Error結(jié)構(gòu)
首先我們?cè)陧?xiàng)目的common目錄中增加errcode目錄,該目錄下會(huì)創(chuàng)建兩個(gè)文件error.go 和 code.go。error.go文件用來(lái)存放自定義Error的結(jié)構(gòu)和相關(guān)方法,code.go 用來(lái)放置項(xiàng)目各種預(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 的原因,比如一個(gè)數(shù)據(jù)庫(kù)查詢語(yǔ)法錯(cuò)誤,拿它再來(lái)生成項(xiàng)目的 AppError 或者是給預(yù)定義好的 AppError 追加上這個(gè)原因的error, 這樣就能達(dá)到保存錯(cuò)誤鏈條的目的。
現(xiàn)在AppError 還不是 error 類型,需要讓他實(shí)現(xiàn)Go的 error interface,這個(gè)接口如下。
type error interface {
Error() string
}
其中只定義了一個(gè)方法,我們讓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對(duì)象的JSON序列化字符串,其中如果cause字段不為空即錯(cuò)誤原因不為空,再去錯(cuò)誤原因的Error方法拿到底層的錯(cuò)誤信息。
我們把Stringer 接口也實(shí)現(xiàn)一下,在沒(méi)有類型字段轉(zhuǎn)換的地方,它還是*AppErr類型,保證這個(gè)時(shí)候序列化它的時(shí)候仍然能得到期望的信息。
func (e *AppError) String() string {
return e.Error()
}
接下來(lái)我們?cè)赾ode.go 中先預(yù)定義一些基礎(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)開(kāi)小差了,請(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ò)多")
)
上面大家看到了 AppError 的類型定義中,字段的訪問(wèn)性都是包內(nèi)可訪問(wèn)的,所以我們要定義一些 getter 方法,這樣接口返回錯(cuò)誤響應(yīng)時(shí),才能讀到錯(cuò)誤碼和錯(cuò)誤信息。
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í)慣是請(qǐng)求接口的響應(yīng)一律是 HTTP 200 再通過(guò)相應(yīng)里的code碼判斷是否正確,這個(gè)方法可以放著不用, 規(guī)范化一點(diǎn)肯定是這種比較好,況且HTTP Status 不是 200 狀態(tài)碼,也是可以返回 code msg 那些信息給客戶端的。
底層Error怎么變成項(xiàng)目Error
上面我們預(yù)定義好了幾個(gè)應(yīng)用錯(cuò)誤,這里說(shuō)明一下,預(yù)定義好的錯(cuò)誤會(huì)最終返回給發(fā)起請(qǐng)求的客戶端,所以控制器層各個(gè)URI的路由處理控制器中最后一定要返回預(yù)定義的錯(cuò)誤,這個(gè)我們會(huì)在未來(lái)給Go項(xiàng)目封裝統(tǒng)一的響應(yīng)組件時(shí)處理。
那一個(gè)底層的錯(cuò)誤怎么才能變成我們自定義的錯(cuò)誤呢?