上一篇文章我们详细讲解了怎么在搭建项目时实现 Go 的error interface 创建项目自己的Error,我们还给这个Error增加了记录错误原因和发生位置的能力。
这一篇文章我们来探讨一下怎么在项目初期提前规划,把项目的各种Error统一管理起来,以及写代码遇到Error时在不同的代码层我们应该怎么处理它们。
如果你想结合项目实战代码学习如何为Go项目定制化自己的应用Error,项目中Error的统一规划管理以及各代码层中Error的处理策略,请访问 :xiaobot.net/p/golang 订阅专栏,即可加入实战项目获得配套的完整实战教程。
怎么把项目的Error管理起来
在聊怎么把Error管理起来之前我们先来聊一下为什么要管理Error,有Error就有Error呗,把信息返回给调用者不就行了?这里说的调用者指的是请求我们系统API的调用方。
乍一想好像确实没毛病,但是咱们把眼光放到团队开发和对接上。
其一:与咱们对接的系统,判断错误一般靠的都是错误响应里的Code码,如果同一个类型的错误你返回不同的错误码,一两个还好,如果十个八个估计对方就要过来找你们算帐了。
其二:既然一类错误的错误码要统一,那每次都自己NewError再设置它的错误码,这样即使只有一个人开发这个项目,次数多了也会把错误码写错的,更别提多个人一起开发的时候了。
所以就需要我们先把常见的能想到的错误预先定义出来。这也就是为什么咱们的项目在error模块的code.go中预先定义了下面几个基础的错误。
ini
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") // 数据转换错误
)
除了这些通用的错误之外,我们可以预先按照项目的模块分配每个业务模块错误的码段。比如在未来的项目代码中你会看到一些给业务模块单独定义的错误码。
ini
// 用户模块相关错误码 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。
go
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。
go
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 如果你不自己做自定义封装确实能给咱们的有效信息很少,我们看到错误信息经常是找半天才能找到原因。
关于如何为Go项目定制化自己的应用Error,项目中Error的统一规划管理以及各代码层中Error的处理策略,这些在我们的专栏中都有详细的解读和实战项目的演示教学。
专栏分为五大部分,目前已经更新20多节教程,主要内容架构如下:
- 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
- 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
- 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
- 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
- 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项