[译]Go如何优雅的处理异常
原文:hackernoon.com/golang-hand…
注:譯文中error可以理解為異常,但Go中的error和Java中的異常還是有很大區(qū)別的,需要讀者慢慢體會(huì),所以為了方便閱讀和思考,譯文中的名詞error就不翻譯了。
正文
??Go有一套簡(jiǎn)單的error處理模型,但其實(shí)并不像看起來(lái)那么簡(jiǎn)單。本文中,我會(huì)提供一種好的方法去處理error,并用這個(gè)方法來(lái)解決在往后編程遇到的的類(lèi)似問(wèn)題。
??首先,我們會(huì)分析下Go中的error。
??接著我們來(lái)看看error的產(chǎn)生和error的處理,再分析其中的缺陷。
??最后,我們將要探索一種方法來(lái)解決我們?cè)诔绦蛑杏龅降念?lèi)似問(wèn)題。
什么是error?
??看下error在內(nèi)置包中的定義,我們可以得出一些結(jié)論:
// error類(lèi)型在內(nèi)置包中的定義是一個(gè)簡(jiǎn)單的接口 // 其中nil代表沒(méi)有異常 type error interface {Error() string } 復(fù)制代碼??從上面的代碼,我們可以看到error是一個(gè)接口,只有一個(gè)Error方法。
??那我們要實(shí)現(xiàn)error就很簡(jiǎn)單了,看以下代碼:
type MyCustomError stringfunc (err MyCustomError) Error() string {return string(err) } 復(fù)制代碼??下面我們用標(biāo)準(zhǔn)包fmt和errors去聲明一些error:
import ("errors""fmt" )simpleError := errors.New("a simple error") simpleError2 := fmt.Errorf("an error from a %s string", "formatted") 復(fù)制代碼??思考:上面的error定義中,只有這些簡(jiǎn)單的信息,就足夠處理好異常嗎?我們先不著急回答,下面我們?nèi)ふ乙环N好的解決方法。
error處理流
??現(xiàn)在我們已經(jīng)知道了在Go中的error是怎樣的了,下一步我們來(lái)看下error的處理流程。
??為了遵循簡(jiǎn)約和DRY(避免重復(fù)代碼)原則,我們應(yīng)該只在一個(gè)地方進(jìn)行error的處理。
??我們來(lái)看下以下的例子:
// 同時(shí)進(jìn)行error處理和返回error // 這是一種糟糕的寫(xiě)法 func someFunc() (Result, error) {result, err := repository.Find(id)if err != nil {log.Errof(err)return Result{}, err}return result, nil } 復(fù)制代碼??上面這段代碼有什么問(wèn)題呢?
??我們首先打印了這個(gè)error信息,然后又將error返回給函數(shù)的調(diào)用者,這相當(dāng)于重復(fù)進(jìn)行了兩次error處理。
??很有可能你組里的同事會(huì)用到這個(gè)方法,當(dāng)出現(xiàn)error時(shí),他很有可能又會(huì)將這個(gè)error打印一遍,然后重復(fù)的日志就會(huì)出現(xiàn)在系統(tǒng)日志里了。
??我們先假設(shè)程序有3層結(jié)構(gòu),分別是數(shù)據(jù)層,交互層和接口層:
// 數(shù)據(jù)層使用了一個(gè)第三方orm庫(kù) func getFromRepository(id int) (Result, error) {result := Result{ID: id}err := orm.entity(&result)if err != nil {return Result{}, err}return result, nil } 復(fù)制代碼??根據(jù)DRY原則,我們可以將error返回給調(diào)用的最上層接口層,這樣我們就能統(tǒng)一的對(duì)error進(jìn)行處理了。
??但上面的代碼有一個(gè)問(wèn)題,Go的內(nèi)置error類(lèi)型是沒(méi)有調(diào)用棧的。另外,如果error產(chǎn)生在第三方庫(kù)中,我們還需要知道我們項(xiàng)目中的哪段代碼負(fù)責(zé)了這個(gè)error。
??github.com/pkg/errors 可以使用這個(gè)庫(kù)來(lái)解決上面的問(wèn)題。
??利用這個(gè)庫(kù),我對(duì)上面的代碼進(jìn)行了一些改進(jìn),加入了調(diào)用棧和加了一些相關(guān)的錯(cuò)誤信息。
import "github.com/pkg/errors"// 使用了第三方的orm庫(kù) 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 } // 當(dāng)error封裝完后,返回的error信息將會(huì)是這樣的 // err.Error() -> error getting the result with id 10 // 這就很容易知道這是來(lái)自orm庫(kù)的error了 復(fù)制代碼??上面的代碼對(duì)orm的error進(jìn)行了封裝,增加了調(diào)用棧,而且沒(méi)有修改原始的error信息。
?? 然后我們?cè)賮?lái)看看在其他層是如何處理這個(gè)error的,首先是交互層:
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) } 復(fù)制代碼??接著是接口層:
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) } 復(fù)制代碼func handleError(w http.ResponseWriter, err error) {// 返回HTTO 500錯(cuò)誤w.WriteHeader(http.StatusIntervalServerError)log.Errorf(err)fmt.Fprintf(w, err.Error()) } 復(fù)制代碼?? 現(xiàn)在我們只在最上層接口層處理了error,看起來(lái)很完美?并不是,如果程序中經(jīng)常返回HTTP錯(cuò)誤碼500,同時(shí)將錯(cuò)誤打印到日志中,像result not found這種沒(méi)用的日志就會(huì)很煩人。
解決方法
?? 我們上面討論到僅僅靠一個(gè)字符串是不足以處理好error的。我們也知道通過(guò)給error加一些額外的信息就能追溯到error的產(chǎn)生和最后的處理邏輯。
?? 因此我定義了三個(gè)error處理的宗旨。
error處理的三個(gè)宗旨
- 提供清晰完整的調(diào)用棧
- 必要時(shí)提供error的上下文信息
- 打印error到日志中(例如可以在框架層打印)
我們來(lái)創(chuàng)建一個(gè)error類(lèi)型:
const(NoType = ErrorType(iota)BadRequestNotFound// 可以加入你需要的error類(lèi)型 )type ErrorType uinttype customError struct {errorType ErrorTypeoriginalError errorcontextInfo map[string]string }// 返回customError具體的錯(cuò)誤信息 func (error customError) Error() string {return error.originalError.Error() }// 創(chuàng)建一個(gè)新的customError func (type ErrorType) New(msg string) error {return customError{errorType: type, originalError: errors.New(msg)} }// 給customError自定義錯(cuò)誤信息 func (type ErrorType) Newf(msg string, args ...interface{}) error {err := fmt.Errof(msg, args...)return customError{errorType: type, originalError: err} }// 對(duì)error進(jìn)行封裝 func (type ErrorType) Wrap(err error, msg string) error {return type.Wrapf(err, msg) }// 對(duì)error進(jìn)行封裝,并加入格式化信息 func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {newErr := errors.Wrapf(err, msg, args..)return customError{errorType: errorType, originalError: newErr} } 復(fù)制代碼?? 從上面的代碼可以看到,我們可以創(chuàng)建一個(gè)新的error類(lèi)型或者對(duì)已有的error進(jìn)行封裝。但我們遺漏了兩件事情,一是我們不知道error的具體類(lèi)型。二是我們不知道怎么給這這個(gè)error加上下文信息。
?? 為了解決以上問(wèn)題,我們來(lái)對(duì)github.com/pkg/errors的方法也進(jìn)行一些封裝。
// 創(chuàng)建一個(gè)NoType error func New(msg string) error {return customError{errorType: NoType, originalError: errors.New(msg)} }// 創(chuàng)建一個(gè)加入了格式化信息的NoType error func Newf(msg string, args ...interface{}) error {return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))} }// 給error封裝多一層string func Wrap(err error, msg string) error {return Wrapf(err, msg) }// 返回最原始的error func Cause(err error) error {return errors.Cause(err) }// error加入格式化信息 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} } 復(fù)制代碼接著我們給error加入上下文信息:
// AddErrorContext adds a context to an error 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} }// GetErrorContext returns the error 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 }// GetType returns the error type func GetType(err error) ErrorType {if customErr, ok := err.(customError); ok {return customErr.errorType}return NoType } 復(fù)制代碼現(xiàn)在將上述的方法應(yīng)用在我們文章開(kāi)頭寫(xiě)的example中:
import "github.com/our_user/our_project/errors" // The repository uses an external depedency orm 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 } // after the error wraping the result will be // err.Error() -> error getting the result with id 10: whatever it comes from the orm 復(fù)制代碼func getInteractor(idString string) (Result, error) {id, 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) }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) } 復(fù)制代碼func handleError(w http.ResponseWriter, err error) {var status interrorType := 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)} } 復(fù)制代碼?? 通過(guò)簡(jiǎn)單的封裝,我們可以明確的知道error的錯(cuò)誤類(lèi)型了,然后我們就能方便進(jìn)行處理了。
?? 讀者也可以將代碼運(yùn)行一遍,或者利用上面的errors庫(kù)寫(xiě)一些demo來(lái)加深理解。
感謝閱讀,歡迎大家指正,留言交流~
轉(zhuǎn)載于:https://juejin.im/post/5cee8ec1518825477a52dee8
總結(jié)
以上是生活随笔為你收集整理的[译]Go如何优雅的处理异常的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SQLI_LAB——Less7~15
- 下一篇: 中国大数据企业排行榜V6.0- 5 年后