引言
儘管go有一個簡單的錯誤模型,但乍一看,事情並不像它們應該的那樣簡單。在這篇文章中,我想提供一個很好的策略來處理錯誤並克服您在過程中可能遇到的問題。
首先,我們將分析go中的error。
然後我們將看到錯誤創建和錯誤處理之間的流程,並分析可能的缺陷。
最後探索一種解決方案,允許我們在不影響應用程序設計的情況下克服這些缺陷。
error
不語言中的錯誤類型是什麼呢?下面是定義我們看一下。
<code>type
errorinterface
{Error
()string
}/<code>
我們看到error是一個接口,它實現了一個返回字符串的簡單方法error。
這個定義告訴我們錯誤就是一個簡單的字符串,所以我們創建下面的結構。
<code>type
MyCustomErrorstring
func
(err MyCustomError)
Error
()
string
{return
string
(err) }/<code>
那要是這樣的話,我想到一個簡單的錯誤定義。
注意:這只是舉個例子。我們可以創建一個錯誤使用go標準包fmt和errors:
<code>import
("errors"
"fmt"
) simpleError := errors.New("a simple error"
) simpleError2 := fmt.Errorf("an error from a %s string"
,"formatted"
)/<code>
這個錯誤處理的寫法是不是很優雅?很簡單。在本文的最後,我們將深入的探討這個問題。
錯誤流處理
上面一小几節,我們已經知道什麼是錯誤。下一步是可視化生命週期中的錯誤流程。
為了簡單期間不要重複寫累贅的代碼。我們把錯誤處理抽象出來。
<code>func
someFunc
()
(Result, error)
{ result, err := repository.Find(id)if
err !=nil
{ log.Errof(err)return
Result{}, err }return
result,nil
}/<code>
上面這段代碼的錯誤處理有什麼不妥之處嗎?原來我們通過首先記錄錯誤,然後又返回錯誤,處理了兩次。
試想如果團隊開發,你的隊友調用了這個錯誤處理函數,然後又手動的打印錯誤日誌。這是不是糟糕極了?
假如我們的應用有3層,repository - interactor - web server,看下面的代碼:
<code>func
getFromRepository
(id
int
)(Result, error)
{ result := Result{ID: id} err := orm.entity(&result)if
err !=nil
{return
Result{}, err }return
result,nil
}/<code>
先是處理邏輯,然後從數據庫拿數據。如果獲取數據失敗,返回故障信息。如果獲取數據正常,直接返回數據。這是通常的做法,也是一種很成熟和穩定的方法。
上面的代碼雖然邏輯上很合理。但是也有一個問題。go語言的錯誤處理沒有堆棧跟蹤,所以如果拋出異常,我們無法追蹤到底是哪一行發生的錯誤。
pkg/errors庫彌補了這個不足。
接著改進上面的代碼。我們明確的指定錯誤拋出位置的信息。
<code>import
"github.com/pkg/errors"
func
getFromRepository
(id
int
)(Result, error)
{ result := Result{ID: id} err := orm.entity(&result)if
err !=nil
{return
Result{}, errors.Wrapf(err,"error getting the result with id %d"
, id); }return
result,nil
}/<code>
經過這樣處理後,發生錯誤時返回的信息如下。
<code>/<code>
這個函數的作用,就是封裝來自ORM的錯誤,在不影響原始信息的情況下,添加了堆棧跟蹤的功能。
在interactor層的用法:
<code>func
getInteractor
(idString
string
)(Result, error)
{ id, err := strconv.Atoi(idString)if
err !=nil
{return
Result{}, errors.Wrapf(err,"interactor converting id to int"
) }return
repository.getFromRepository(id) }/<code>
頂層web server的用法:
<code>r := mux.NewRouter() r.HandleFunc("/result/{id}"
, ResultHandler)func
ResultHandler
(w http.ResponseWriter, r *http.Request)
{ vars := mux.Vars(r) result, err := interactor.getInteractor(vars["id"
])if
err !=nil
{ handleError(w, err) } fmt.Fprintf(w, result) }func
handleError
(w http.ResponseWriter, err error)
{ w.WriteHeader(http.StatusIntervalServerError) log.Errorf(err) fmt.Fprintf(w, err.Error()) }/<code>
大家看在頂層處理錯誤,完美嗎?不完美。為什麼呢?因為都是一些500的HTTP CODE,沒什麼用,給日誌文件添加的都是無用的數據。
優雅的用法
上一段您也看到了,在web server層處理錯誤,不完美啊,都混沌了。
我們知道,如果我們在錯誤中引入新的內容,我們將以某種方式在創建錯誤的地方和最終處理錯誤的時候引入依賴項。
所以讓我們來探索一個定義3個目標的解決方案:
- 提供良好的錯誤堆棧跟蹤
- web層面的錯誤日誌
- 必要時為用戶提供上下文錯誤信息。(例如:所提供的電子郵件格式不正確)
首先創建一個錯誤類型。
<code>package
errorsconst
( NoType = ErrorType(iota
) BadRequest NotFound )type
ErrorTypeuint
type
customErrorstruct
{ errorType ErrorType originalError error contextInfomap
[string
]string
}func
(error customError)
Error
()
string
{return
error.originalError.Error() }func
(
type
ErrorType)New
(msg
string
)error
{return
customError{errorType:type
, originalError: errors.New(msg)} }func
(
type
ErrorType)Newf
(msg
string
, args ...interface
{})error
{ err := fmt.Errof(msg, args...)return
customError{errorType:type
, originalError: err} }func
(
type
ErrorType)Wrap
(err error, msg
string
)error
{return
type
.Wrapf(err, msg) }func
(
type
ErrorType)Wrapf
(err error, msg
string
, args ...interface
{})error
{ newErr := errors.Wrapf(err, msg, args..)return
customError{errorType: errorType, originalError: newErr} }/<code>
正如上面代碼所示,只有ErrorType 和錯誤類型是公開可訪問的。我們可以創建任意新的錯誤,或修飾已存在的錯誤。
但是有兩件事情沒有做到:
- 如何在不導出customError的情況下檢查錯誤類型?
- 我們如何向錯誤中添加/獲取上下文,甚至是向外部依賴項中已存在的錯誤中添加上下文?
改進上面的代碼:
<code>func
New
(msg
string
)error
{return
customError{errorType: NoType, originalError: errors.New(msg)} }func
Newf
(msg
string
, args ...interface
{})error
{return
customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))} }func
Wrap
(err error, msg
string
)error
{return
Wrapf(err, msg) }func
Cause
(err error)
error
{return
errors.Cause(err) }func
Wrapf
(err error, msg
string
, args ...interface
{})error
{ wrappedError := errors.Wrapf(err, msg, args...)if
customErr, ok := err.(customError); ok {return
customError{ errorType: customErr.errorType, originalError: wrappedError, contextInfo: customErr.contextInfo, } }return
customError{errorType: NoType, originalError: wrappedError} }/<code>
現在讓我們建立我們的方法處理上下文和任何一般錯誤的類型:
<code>func
AddErrorContext
(err error, field, message
string
)error
{ context := errorContext{Field: field, Message: message}if
customErr, ok := err.(customError); ok {return
customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context} }return
customError{errorType: NoType, originalError: err, contextInfo: context} }func
GetErrorContext
(err error)
map
[string
]string
{ emptyContext := errorContext{}if
customErr, ok := err.(customError); ok || customErr.contextInfo != emptyContext {return
map
[string
]string
{"field"
: customErr.context.Field,"message"
: customErr.context.Message} }return
nil
}func
GetType
(err error)
ErrorType
{if
customErr, ok := err.(customError); ok {return
customErr.errorType }return
NoType }/<code>
現在回到我們的例子,我們要應用這個新的錯誤包:
<code>import
"github.com/our_user/our_project/errors"
func
getFromRepository
(id
int
)(Result, error)
{ result := Result{ID: id} err := orm.entity(&result)if
err !=nil
{ msg := fmt.Sprintf("error getting the result with id %d"
, id)switch
err {case
orm.NoResult: err = errors.Wrapf(err, msg);default
: err = errors.NotFound(err, msg); }return
Result{}, err }return
result,nil
}/<code>
interactor層的寫法:
<code>func
getInteractor(idString string) (Result, error) {
err := strconv.Atoi(idString)
if
err != nil {
err
=errors.BadRequest.Wrapf(err, "interactor converting id to int")
err
=errors.AddContext(err, "id", "wrong id format, should be an integer)
return
Result{}, err
}
return
repository.getFromRepository(id)
}
/<code>
最後web server層的寫法:
<code>r := mux.NewRouter() r.HandleFunc("/result/{id}"
, ResultHandler)func
ResultHandler
(w http.ResponseWriter, r *http.Request)
{ vars := mux.Vars(r) result, err := interactor.getInteractor(vars["id"
])if
err !=nil
{ handleError(w, err) } fmt.Fprintf(w, result) }func
handleError
(w http.ResponseWriter, err error)
{var
statusint
errorType := errors.GetType(err)switch
errorType {case
BadRequest: status = http.StatusBadRequestcase
NotFound: status = http.StatusNotFounddefault
: status = http.StatusInternalServerError } w.WriteHeader(status)if
errorType == errors.NoType { log.Errorf(err) } fmt.Fprintf(w,"error %s"
, err.Error()) errorContext := errors.GetContext(err)if
errorContext !=nil
{ fmt.Printf(w,"context %v"
, errorContext) } }/<code>
寫在最後
大家看到了,使用導出的類型和一些導出的值,我們可以更輕鬆地處理錯誤。
這個解決方案在創建錯誤時,也顯式地顯示了錯誤的類型,這很贊!