运用 go 开发,会经常和 error 打交道,但至今,官方供给的 error 实在是无力吐槽,虽然从 1.13 供给了一个 WrapUnWrap 方法,但是还是无法满意咱们日常开发的需求。所以咱们不得不对它进行二次封装。今天和咱们共享下,在咱们实践的项目开发中的一些关于 error 实践经验。

首先来说说实践开发中咱们对 error 的需求:

err 需求

  1. 咱们期望 error 要带栈信息,便利出错时能定位到代码是哪一行出错;
  2. 原始过错不能直接暴露给前端(比方db过错)等,所以需求支撑过错包装功用,包装过的过错用于产品侧显现,原始过错用于体系日志;
  3. 能够界说过错码,用以区归类不同的过错类别,比方参数验证类,体系类过错。

这三个需求是咱们在做业务项目惯例需求,官方的 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.

过错分类

现在来说,咱们把服务的过错首要分类两大类:

  1. 参数类过错
  2. 体系类过错

咱们并不是为每一种过错都界说了过错类型,而是把服务的过错分为了常用的两类。参数类过错用于阐明接口参数验证类别过错;而体系类过错表明过错由内部发生的,而非来自用户。

就现在而言,咱们觉得现已足够,咱们并没在把服务端过错再细分,比方:db errorredis 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 包装一个过错栈,不会供给友爱的过错描绘,和过错码包装,假如要考虑前端输出,还需求你自己来安排。