自定义业务错误信息

以下的文章是管大佬要的学习资料,分享给大家,也当一个记录。原出处,我无从寻找,非常抱歉!
————————————————————————————————————

本节核心内容

  • 如何自定义业务自己的错误信息
  • 实际开发中是如何处理错误的
  • 实际开发中常见的错误类型
  • 通过引入新包 errno 来实现此功能,会展示该包的如下用法:
    • 如何新建 Err 类型的错误
    • 如何从 Err 类型的错误中获取 code 和 message

本小节源码下载路径:demo05

可先下载源码到本地,结合源码理解后续内容,边学边练。

本小节的代码是基于 demo04 来开发的。

为什么要定制业务自己的错误码

在实际开发中引入错误码有如下好处:

  • 可以非常方便地定位问题和定位代码行(看到错误码知道什么意思,grep 错误码可以定位到错误码所在行)
  • 如果 API 对外开放,有个错误码会更专业些
  • 错误码包含一定的信息,通过错误码可以判断出错误级别、错误模块和具体错误信息
  • 在实际业务开发中,一个条错误信息需要包含两部分内容:直接展示给用户的 message 和用于开发人员 debug 的 errormessage 可能会直接展示给用户,error 是用于 debug 的错误信息,可能包含敏感/内部信息,不宜对外展示
  • 业务开发过程中,可能需要判断错误是哪种类型以便做相应的逻辑处理,通过定制的错误码很容易做到这点,例如:
    if err == errno.ErrBind {
        ...
    }   

  • Go 中的 HTTP 服务器开发都是引用 net/http 包,该包中只有 60 个错误码,基本都是跟 HTTP 请求相关的。在大型系统中,这些错误码完全不够用,而且跟业务没有任何关联,满足不了业务需求。

在 apiserver 中引入错误码

我们通过一个新包 errno 来做错误码的定制,详见 demo05/pkg/errno。

$ ls pkg/errno/
code.go  errno.go

errno 包由两个 Go 文件组成:code.goerrno.gocode.go 用来统一存自定义的错误码,code.go 的代码为:

package errno

var (
    // Common errors
    OK                  = &Errno{Code: 0, Message: "OK"}
    InternalServerError = &Errno{Code: 10001, Message: "Internal server error"}
    ErrBind             = &Errno{Code: 10002, Message: "Error occurred while binding the request body to the struct."}

    // user errors
    ErrUserNotFound      = &Errno{Code: 20102, Message: "The user was not found."}
)

代码解析

在实际开发中,一个错误类型通常包含两部分:Code 部分,用来唯一标识一个错误;Message 部分,用来展示错误信息,这部分错误信息通常供前端直接展示。这两部分映射在 errno 包中即为 &Errno{Code: 0, Message: "OK"}

错误码设计

目前错误码没有一个统一的设计标准,笔者研究了 BAT 和新浪开放平台对外公布的错误码设计,参考新浪开放平台 Error code 的设计,如下是设计说明:

错误返回值格式:

{
  "code": 10002,
  "message": "Error occurred while binding the request body to the struct."
}

错误代码说明:

图片:https://user-gold-cdn.xitu.io/2018/6/1/163b938084a7e9e9?w=1098&h=131&f=png&s=9673

  • 服务级别错误:1 为系统级错误;2 为普通错误,通常是由用户非法操作引起的
  • 服务模块为两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了
  • 错误码为两位数:防止一个模块定制过多的错误码,后期不好维护
  • code = 0 说明是正确返回,code > 0 说明是错误返回
  • 错误通常包括系统级错误码和服务级错误码
  • 建议代码中按服务模块将错误分类
  • 错误码均为 >= 0 的数
  • 在 apiserver 中 HTTP Code 固定为 http.StatusOK,错误码通过 code 来表示。

错误信息处理

通过 errno.go 来对自定义的错误进行处理,errno.go 的代码为:

package errno

import "fmt"

type Errno struct {
    Code    int
    Message string
}

func (err Errno) Error() string {
    return err.Message
}

// Err represents an error
type Err struct {
    Code    int
    Message string
    Err     error
}

func New(errno *Errno, err error) *Err {
    return &Err{Code: errno.Code, Message: errno.Message, Err: err}
}

func (err *Err) Add(message string) error {
    err.Message += " " + message
    return err
}

func (err *Err) Addf(format string, args ...interface{}) error {
    err.Message += " " + fmt.Sprintf(format, args...)
    return err
}

func (err *Err) Error() string {
    return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.Code, err.Message, err.Err)
}

func IsErrUserNotFound(err error) bool {
    code, _ := DecodeErr(err)
    return code == ErrUserNotFound.Code
}

func DecodeErr(err error) (int, string) {
    if err == nil {
        return OK.Code, OK.Message
    }

    switch typed := err.(type) {
    case *Err:
        return typed.Code, typed.Message
    case *Errno:
        return typed.Code, typed.Message
    default:
    }

    return InternalServerError.Code, err.Error()
}

代码解析

errno.go 源码文件中有两个核心函数 New()DecodeErr(),一个用来新建定制的错误,一个用来解析定制的错误,稍后会介绍如何使用。

errno.go 同时也提供了 Add()Addf() 函数,如果想对外展示更多的信息可以调用此函数,使用方法下面有介绍。

错误码实战

上面介绍了错误码的一些知识,这一部分讲开发中是如何使用 errno 包来处理错误信息的。为了演示,我们新增一个创建用户的 API:

  1. router/router.go 中添加路由,详见 demo05/router/router.go:

  2. handler 目录下增加业务处理函数 handler/user/create.go,详见 demo05/handler/user/create.go。

编译并运行

  1. 下载 apiserver_demos 源码包(如前面已经下载过,请忽略此步骤)
$ git clone https://github.com/lexkong/apiserver_demos

  1. apiserver_demos/demo05 复制为 $GOPATH/src/apiserver
$ cp -a apiserver_demos/demo05/ $GOPATH/src/apiserver

  1. 在 apiserver 目录下编译源码
$ cd $GOPATH/src/apiserver
$ gofmt -w .
$ go tool vet .
$ go build -v .

测试验证

启动 apiserver:./apiserver

$ curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user

{
  "code": 10002,
  "message": "Error occurred while binding the request body to the struct."
}

因为没有传入任何参数,所以返回 errno.ErrBind 错误。

$ curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"username":"admin"}'

{
  "code": 10001,
  "message": "password is empty"
}

因为没有传入 password,所以返回 fmt.Errorf("password is empty") 错误,该错误信息不是定制的错误类型,errno.DecodeErr(err) 解析时会解析为默认的 errno.InternalServerError 错误,所以返回结果中 code 为 10001,message 为 err.Error()

$ curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"password":"admin"}'

{
  "code": 20102,
  "message": "The user was not found. This is add message."
}

因为没有传入 username,所以返回 errno.ErrUserNotFound 错误信息,并通过 Add() 函数在 message 信息后追加了 This is add message. 信息。

通过

   if errno.IsErrUserNotFound(err) {
        log.Debug("err type is ErrUserNotFound")
    }   

演示了如何通过定制错误方便地对比是不是某个错误,在该请求中,apiserver 会输出错误(建议尝试一下,看看错误是什么)

可以看到在后台日志中会输出敏感信息 username can not found in db: xx.xx.xx.xx,但是返回给用户的 message ({"code":20102,"message":"The user was not found. This is add message."})不包含这些敏感信息,可以供前端直接对外展示。

$ curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"username":"admin","password":"admin"}'

{
  "code": 0,
  "message": "OK"
}

如果 err = nil,则 errno.DecodeErr(err) 会返回成功的 code: 0message: OK

如果 API 是对外的,错误信息数量有限,则制定错误码非常容易,强烈建议使用错误码。如果是内部系统,特别是庞大的系统,内部错误会非常多,这时候没必要为每一个错误制定错误码,而只需为常见的错误制定错误码,对于普通的错误,系统在处理时会统一作为 InternalServerError 处理。

小结

本小节详细介绍了实际开发中是如何处理业务错误信息的,并给出了笔者倾向的错误码规范供读者参考,最后通过大量的实例来展示如何通过 errno 包来处理不同场景的错误。

你可能感兴趣的:(自定义业务错误信息)