引言
尽管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>
写在最后
大家看到了,使用导出的类型和一些导出的值,我们可以更轻松地处理错误。
这个解决方案在创建错误时,也显式地显示了错误的类型,这很赞!