运用 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
包装一个过错栈,不会供给友爱的过错描绘,和过错码包装,假如要考虑前端输出,还需求你自己来安排。