这一篇文章我们来探讨一下怎么在项目初期提前规划,把项目的各种Error统一管理起来,以及写代码遇到Error时在不同的代码层我们应该怎么处理它们。
图片
怎么把项目的Error管理起来
在聊怎么把Error管理起来之前我们先来聊一下为什么要管理Error,有Error就有Error呗,把信息返回给调用者不就行了?这里说的调用者指的是请求我们系统API的调用方。
乍一想好像确实没毛病,但是咱们把眼光放到团队开发和对接上。
其一:与咱们对接的系统,判断错误一般靠的都是错误响应里的Code码,如果同一个类型的错误你返回不同的错误码,一两个还好,如果十个八个估计对方就要过来找你们算帐了。
其二:既然一类错误的错误码要统一,那每次都自己NewError再设置它的错误码,这样即使只有一个人开发这个项目,次数多了也会把错误码写错的,更别提多个人一起开发的时候了。
所以就需要我们先把常见的能想到的错误预先定义出来。这也就是为什么咱们的项目在error模块的code.go中预先定义了下面几个基础的错误。
var (
Success = newError(0, "success")
ErrServer = newError(10000000, "服务器内部错误")
ErrParams = newError(10000001, "参数错误, 请检查")
ErrNotFound = newError(10000002, "资源未找到")
ErrPanic = newError(10000003, "(*^__^*)系统开小差了,请稍后重试") // 无预期的panic错误
ErrToken = newError(10000004, "Token无效")
ErrForbidden = newError(10000005, "未授权") // 访问一些未授权的资源时的错误
ErrTooManyRequests = newError(10000006, "请求过多")
ErrCoverData = newError(10000007, "ConvertDataError") // 数据转换错误
)
除了这些通用的错误之外,我们可以预先按照项目的模块分配每个业务模块错误的码段。比如在未来的项目代码中你会看到一些给业务模块单独定义的错误码。
// 用户模块相关错误码 10000100 ~ 1000199
var (
ErrUserInvalid = newError(10000101, "用户异常")
ErrUserNameOccupied = newError(10000102, "用户名已被占用")
...
)
// 商品模块相关错误码 10000200 ~ 1000299
var (
ErrCommodityNotExists = newError(10000200, "商品不存在")
ErrCommodityStockOut = newError(10000201, "库存不足")
...
)
// 购物车模块相关错误码 10000300 ~ 1000399
var (
ErrCartItemParam = newError(10000300, "购物项参数异常")
ErrCartWrongUser = newError(10000301, "用户购物信息不匹配")
...
)
到这里一直说的都是预先定义错误,那针对一些不知道什么类型的错误该怎么办?比如在DAO层做了一下CRUD出现了Error,难道还要预先定义一个ErrDBQuery 之类的错误吗?那项目用的中间件多了,Redis、MQ什么的都要预先定义错误吗?
这里我给我的方案是,调用其他外部基础组件出错时,调用一个SDK方法出错时,把底层错误包装成项目的Error。
func Wrap(msg string, err error) *AppError {
if err == nil {
return nil
}
appErr := &AppError{code: -1, msg: msg, cause: err}
return appErr
}
当你拿到一个error不确定它该是什么错误,你就用这个Wrap方法包装成项目的App Error。
下一节我们封装的统一接口响应组件会使用下面的方法来获取Error对应的HTTP Code。
func (e *AppError) HttpStatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ErrParams.Code():
return http.StatusBadRequest
case ErrNotFound.Code():
......
default:
return http.StatusInternalServerError
}
}
不同代码层该怎么处理Error
我们在写代码的时候为了保险,都爱在 error 判断中打一条Error级别的日志,这样好歹遇到错误了在日志中会留下痕迹,到了真需要排除问题的时候总比那些什么日志都不记录的写法要好多了。
很多时候我们遇到线上问题了,查半天最后实现没办法就加几条日志部署上去观察观察情况,等同样的错误发生了再去看新打的日志。
但是不知道大家有没有发现,如果你每遇到Error都打一条日志的话,那么这个错误信息在日志里的重复率时相当的高,发生了一个错误,好几条日志都是这个错误信息,其实都是同一个错误,只不过这些日志是在调用逻辑的不同代码层做被打进去的。
那么关于什么错误该记日志,什么不该记,有没有什么好用的标准?不好意思没有,全靠自己的悟性。。。。。。听到这里是不是想骂人了。
这里分享一下国外论坛中经常看到的 Only handle errors once的原则
Go程序错误处理的原则:
- 程序底层 (Dao、基础设施层) 抛出错误
- 程序中层(领域服务层、应用服务层)包装错误
- 程序上层(控制层) 记录错误
如果每一层都打日志,查询日志的时候必然会有不少重复,当然这个见仁见智,多打点日志也没错总比不打日志,出问题了再打日志,等线上复现问题后再排查日志要强多了。
还有一个原因就是Go的原生 Error 如果你不自己做自定义封装确实能给咱们的有效信息很少,我们看到错误信息经常是找半天才能找到原因。