go项目开发中关于error实践总结
使用 go
开发,会经常和 error
打交道,但至今,官方提供的 error
实在是无力吐槽,虽然从 1.13
提供了一个 Wrap
和 UnWrap
方式,但是还是无法满足我们日常开发的需求。所以我们不得不对它进行二次封装。今天和大家分享下,在我们实际的项目开发中的一些关于 error
实践经验。
首先来说说实际开发中我们对 error
的需求:
err 需求
- 我们希望
error
要带栈信息,方便出错时能定位到代码是哪一行出错; - 原始错误不能直接暴露给前端(比如db错误)等,所以需要支持错误包装功能,包装过的错误用于产品侧显示,原始错误用于系统日志;
- 可以定义错误码,用以区归类不同的错误类别,比如参数验证类,系统类错误。
这三个需求是我们在做业务项目常规需求,官方的 error
肯定没法直接使用,所以实际中我们自行封装了一些 error
辅助类,来帮我们满足如上的需求。
其实行业里这种错误包比较知名的:pkg/error,但我们发觉这个包也有一些缺陷,比如它把包装错误和原始错误信息合并在一起输出,而不是分开。这导致上述的第2小点无法满足;并且它提供的栈信息是全链路的,栈的路径会比较多,而在实际操作中我们发觉,我们最迫切需要的也仅仅只是 error
产生的那一行,后续的栈其实并不是那么重要,而且栈的深度如果只有一层,也能减少系统的错误日志量,节省带宽资源。
介于此,我们打算实现自己的 error
错误封装。
err 设计
我们自定义了一个 CodeErr
类型,并实现了 Error()
接口函数,这个错误类型,有三个成员属性,code
: 错误码,msg
: 包装的错误消息,cause
:真正的原始错误。
type CodeErr struct {
code int
msg string
cause error
}
func (e *CodeErr) Error() string {
return e.msg
}
func (e *CodeErr) Code() int {
return e.code
}
func (e *CodeErr) Cause() error { return e.cause }
func (e *CodeErr) Unwrap() error { return e.cause }
func (e *CodeErr) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
_, _ = fmt.Fprintf(s, "%+v \n", e.Cause())
_, _ = io.WriteString(s, e.msg)
return
}
fallthrough
case 's', 'q':
_, _ = io.WriteString(s, e.Error())
}
}
注意,通过 Error()
接口返回的错误 msg
是包装的描述信息,而不是原始错误,这是有意设计的。这样设计我们就可以直接很多时候把错误返回给前端。
除此外,我们还继承 Format
接口,重写了 %+v
flag,方便我们输出原始的错误日志。即:
默认输出包装过的友好错误,用于直接返回给前端:
// 输出包装错误, 内部调用 Error() 方法
fmt.Println(err)
使用%+v
输出原始错误:
// 输出原始错误,日志上报
fmt.Sprintf("%+v", err)
错误栈
光有上面的带错误码 error
还不够,我们还希望要有栈信息,所以我们还需要封装另一个错误类型。
type withStack struct {
error
*stack
}
完整代码请参考:github.com/ntt360/gin/…
我们借鉴了 pkg/error
的栈封装方式,但有所改进,即 %+v
输出的时候,仅会输出原始错误,而不会同时输出包装过的错误。此外栈的默认深度是1,即默认只提供错误触发的所在行(但可全局自定义)。
// 全局设置栈的深度
gin.SetErrStackNum(num int)
此外,我们之所以把栈错误和代码错误分开,主要是考虑代码复用性,可能有时候你单纯想要一个带栈的 error
.
错误分类
目前来说,我们把服务的错误主要分类两大类:
- 参数类错误
- 系统类错误
我们并不是为每一种错误都定义了错误类型,而是把服务的错误分为了常用的两类。参数类错误用于说明接口参数验证类别错误;而系统类错误表示错误由内部产生的,而非来自用户。
就目前而言,我们觉得已经足够,我们并没在把服务端错误再细分,比如:db
error
,redis
error
等等,因为原始的错误已经可以很详细描述这是什么错误,我们目前没有归类的需求,而且不管是哪种错误,都需要我们去定位排查。所以同归为系统错误。
因此,我们定义了两类错误码:
CodeServerErr = 1 // 服务器错误
CodeParamNotValid = 2 // 参数验证失败
错误函数封装
有了上述基础错误定义以及错误类别,为了在项目中,方便使用,我们还定义了一系列的错误函数来辅助我们使用。
// 带栈的错误
func WithStack(err error) error
// NewCodeErrf 自定义Code码的错误消息
func NewCodeErrf(code int, format string, a ...any) error
func WrapCodeErrf(err error, code int, format string, a ...any) error
func WrapParamErrf(err error, format string, a ...any) error
func WrapSysErrf(err error, format string, a ...any) error
func WrapDefaultSysErr(err error) error
// NewParamErrf 参数类型错误,自定义消息内容,支持格式化内容
func NewParamErrf(format string, a ...any) error
// DefaultSysErr 默认系统错误,即提供默认的错误码,和错误描述
func DefaultSysErr() error
// NewSysErrf 系统类型错误,支持自定义错误格式
func NewSysErrf(format string, a ...any) error
完整代码来自于项目:github.com/ntt360/gin/…,我们将在下节中分享具体的使用场景。
实际使用
下面是我们在实际项目中如何利用上述封装,实际使用场景:
参数类错误
1. 仅修改错误描述
很多时候,服务端需要验证接口请求参数,并返回前端错误。这时候可以:
if len(ctx.Query("size")) == 0 {
return e.NewParamErrf("size must required")
}
NewParamErrf()
函数用于包装一个友好的错误消息,方便前端展示:
{
"errno": 2,
"msg": "size must required",
"data": null
}
而且在开发控制台,则会输出详细的错误栈信息,方便开发调试:
<nil>
size must required
test/app/http/controllers/home.Index
/Users/xxxx/IdeaProjects/test/app/http/controllers/home/home.go:15
所有的错误封装,都带有栈信息,这方便定位错误。
2. 包装原始错误,返回参数错误
如果已经存在一个既有的错误,我们希望包装错误描述,返回参数错误类型,那么可以使用:
err = e.WrapParamErrf(err, "the param not valid")
同理,前端将会输出:
{
"errno": 2,
"msg": "the param not valid",
"data": null
}
而内部栈错误,和之前类似。
系统类错误
很多时候系统内部会产生一些错误,比如数据库异常,网络超时等等。那么可以使用如下一些函数:
1. 默认系统错误
err = e.DefaultSysErr()
前端输出:
{
"errno": 1,
"msg": "server err",
"data": null
}
而控制台则会输出类似:
<nil>
server err
test/app/http/controllers/home.Index
/Users/xxxxx/IdeaProjects/test/app/http/controllers/home/home.go:14
DefaultSysErr()
返回的是系统默认错误模板,如果存在原始错误,那么可以使用系统类包装函数系列。
2 系统包装错误
err = app.DbR(ctx).Raw("select * from user limit 1").First(&models.User).Error
if err != nil {
return e.WrapSysErrf(err, "db err")
}
前端将返回:
{
"errno": 1,
"msg": "db err",
"data": null
}
控制台栈信息错误类似,会输出原始的数据库错误。
自定义错误码
有时候,你可能希望既定义错误,也希望修改错误码,那么可以使用:
func WrapCodeErrf(err error, code int, format string, a ...any)
func NewCodeErrf(code int, format string, a ...any) error
这些函数方便你包装错误消息,也同时方便你修改 errno
,用法和之前方法类似。
栈错误
如果有时候,你仅仅希望包装一个栈错误,那么你可以单独使用:
e.WithStack(err)
WithStack()
仅对 err
包装一个错误栈,不会提供友好的错误描述,和错误码包装,如果要考虑前端输出,还需要你自己来组织。
转载自:https://juejin.cn/post/7182202759876182075