引言
尽管go有一个简单的错误模型,但乍一看,事情并不像它们应该的那样简单。在这篇文章中,我想提供一个很好的策略来处理错误并克服您在过程中可能遇到的问题。
首先,我们将分析go中的error。
然后我们将看到错误创建和错误处理之间的流程,并分析可能的缺陷。
最后探索一种解决方案,允许我们在不影响应用程序设计的情况下克服这些缺陷。
error
不语言中的错误类型是什么呢?下面是定义我们看一下。
// The error built-in interface type is the conventional interface for// representing an error condition, with the nil value representing no error.type error interface {Error() string}
我们看到error是一个接口,它实现了一个返回字符串的简单方法error。
这个定义告诉我们错误就是一个简单的字符串,所以我们创建下面的结构。
type MyCustomError stringfunc (err MyCustomError) Error() string { return string(err)}
那要是这样的话,我想到一个简单的错误定义。
注意:这只是举个例子。我们可以创建一个错误使用go标准包fmt和errors:
import ( "errors" "fmt")simpleError := errors.New("a simple error")simpleError2 := fmt.Errorf("an error from a %s string", "formatted")
这个错误处理的写法是不是很优雅?很简单。在本文的最后,我们将深入的探讨这个问题。
错误流处理
上面一小几节,我们已经知道什么是错误。下一步是可视化生命周期中的错误流程。
为了简单期间不要重复写累赘的代码。我们把错误处理抽象出来。
func someFunc() (Result, error) { result, err := repository.Find(id) if err != nil { log.Errof(err) return Result{}, err } return result, nil}
上面这段代码的错误处理有什么不妥之处吗?原来我们通过首先记录错误,然后又返回错误,处理了两次。
试想如果团队开发,你的队友调用了这个错误处理函数,然后又手动的打印错误日志。这是不是糟糕极了?
假如我们的应用有3层,repository - interactor - web server,看下面的代码:
func getFromRepository(id int) (Result, error) { result := Result{ID: id} err := orm.entity(&result) if err != nil { return Result{}, err } return result, nil }
先是处理逻辑,然后从数据库拿数据。如果获取数据失败,返回故障信息。如果获取数据正常,直接返回数据。这是通常的做法,也是一种很成熟和稳定的方法。
上面的代码虽然逻辑上很合理。但是也有一个问题。go语言的错误处理没有堆栈跟踪,所以如果抛出异常,我们无法追踪到底是哪一行发生的错误。
pkg/errors库弥补了这个不足。
接着改进上面的代码。我们明确的指定错误抛出位置的信息。
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 }
经过这样处理后,发生错误时返回的信息如下。
// err.Error() -> error getting the result with id 10: whatever it comes from the orm
这个函数的作用,就是封装来自ORM的错误,在不影响原始信息的情况下,添加了堆栈跟踪的功能。
在interactor层的用法:
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) }
顶层web server的用法:
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())}
大家看在顶层处理错误,完美吗?不完美。为什么呢?因为都是一些500的HTTP CODE,没什么用,给日志文件添加的都是无用的数据。
优雅的用法
上一段您也看到了,在web server层处理错误,不完美啊,都混沌了。
我们知道,如果我们在错误中引入新的内容,我们将以某种方式在创建错误的地方和最终处理错误的时候引入依赖项。
所以让我们来探索一个定义3个目标的解决方案:
- 提供良好的错误堆栈跟踪
- web层面的错误日志
- 必要时为用户提供上下文错误信息。(例如:所提供的电子邮件格式不正确)
首先创建一个错误类型。
package errorsconst( NoType = ErrorType(iota) BadRequest NotFound //添加任意其他项。)type ErrorType uinttype customError struct { errorType ErrorType originalError error contextInfo map[string]string }// Error 函数返回 customError func (error customError) Error() string { return error.originalError.Error()}// 新建一个 customError 结构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}}
正如上面代码所示,只有ErrorType 和错误类型是公开可访问的。我们可以创建任意新的错误,或修饰已存在的错误。
但是有两件事情没有做到:
- 如何在不导出customError的情况下检查错误类型?
- 我们如何向错误中添加/获取上下文,甚至是向外部依赖项中已存在的错误中添加上下文?
改进上面的代码:
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}}
现在让我们建立我们的方法处理上下文和任何一般错误的类型:
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}
现在回到我们的例子,我们要应用这个新的错误包:
import "github.com/our_user/our_project/errors"// The repository 使用了外部依赖项 ormfunc 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 }
interactor层的写法:
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) }
最后web server层的写法:
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 status int errorType := errors.GetType(err) switch errorType { case BadRequest: status = http.StatusBadRequest case NotFound: status = http.StatusNotFound default: 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) }}
写在最后
大家看到了,使用导出的类型和一些导出的值,我们可以更轻松地处理错误。
这个解决方案在创建错误时,也显式地显示了错误的类型,这很赞!