Golang中Error处理方案

bysir · · 15122 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

error 与 try catch 对比

有人说err写得繁琐,而我却觉得更准确,清晰。更明确的说明这个函数可能有错,提示程序猿需要显示处理。

当然在使用err之前,我也没觉得java那样的try catch有什么问题,不过现在我更喜欢err。

我们来对比一下两者的优劣,从写法和排错方法两个方面。

写法

在编写被调用函数方两者基本差别不大,一边需要写err 一边需要throw。

在调用方try的方式可以一次捕获多个thower,而err就需要繁琐的写多次。

当然try也有坏处,try会导致代码缩进一层。

排错

try看问题必须看错误堆栈,而我发现堆栈中80%的字母都是无用的。

而err的错误信息就一行,清晰明了。

那有人说了,就一行这么少信息,怎么定位是谁调用了它,怎么确定入参是什么才引起的错误呢?

为了解释一行错误信息也能定位问题,我先提出一个概念:

一个函数应该是完善的,确定的。

先看例子再来说概念吧,这样更好理解。

// 糟糕的代码
// 入参模糊, 当发生错误, 必须依赖上层参数才能找到错误.
func Insert(data interface{}) (err error) {
    err = mysql.Insert(data)
    if err != nil {
        return
    }
    return
}

// 正确的代码
// 确定性: 入参确定
func InsertUser(user *User) (err error) {
    // 完善性: 函数应该自己能够判断一个入参是否非法并立刻返回错误
    if user == nil {
        err = ...
        return
    }
    if user.UserName == "" {
        err = ...
        return
    }
    err = mysql.Insert(user)
    if err != nil {
        return
    }
    return
}

完善的: 自己管理自己的入参,函数应该自己能够判断一个入参是否非法并立刻返回错误,不要等到最后报一个模棱两可的错。

确定的: 一个函数做一件事情,并且入参和出参是确定的,这样无论多少个调用者来调用这个函数,都不会因为入参不同而需要上层调用者信息来排查问题。

可以看到,排错的简易程度和错误堆栈并没有必然关系,而和编写代码的人有必然关系。

error的问题

我认为err的缺陷也很明显,由于没有错误堆栈,如果只有一个string提示的话,当我们程序比较复杂,层级较多时,我们很难定位到是那一层出错了。

那么又矛盾了,那还是需要错误堆栈的呀?

并不是,我们再理一下思路,想一想我们到底需要什么。

在这之前,我们先要区分开两种错误:意料之中的和意料之外的。

意料之中的err

意料之中指的是前端请求的参数不按照规定来,这种错误我们不需要打印错误日志,甚至可以不用管。不过为了前端让前端知道是哪个参数有问题,后端还是需要返回一个错误信息。
这时候需要:

  • 能让前端清晰知道缘由的错误提示。如:"id不能为空"。

不过事实没这么简单,当一个api会做两件事情的时候,前端需要得到更详细的错误信息:

  • ”插入订单表错误:id不能为空“
  • ”插入商品表错误:id不能为空“

一般来说”id不能为空“是被调用函数报的,而”插入订单表错误“则是由调用方报的。这时候就涉及到err的拼接问题,先把这个问题记下来,待会解决。

意料之外的err

意料之外指的是服务器异常,如数据库连接不上,这个时候错误不需要告诉前端,而是应该收集起来发送给后端开发人员。
这时候就需要有:

  • 错误原因,这个当然是必须的。如数据库连接不上。
  • 错误位置,哪一个文件哪一行代码。这个也是必须的。

区分了两种错误事情就简单多了。

下面开始写代码

优化error

主要思想有几点:

  • 使用错误码区分上述”意料之中“与”意料之外“(下文会用"错误"和"异常"代替)
  • 使用runtime.Caller来获取当前行数
  • 使用warp提供更多信息来代替”堆栈“

那接下来就看简化版代码吧 (伪代码)

定义错误

type ErrorCode struct {
    code  uint32
    msg   string
    where string
}
func (e *ErrorCode) Error() string {
    return fmt.Sprintf("code = %d ; msg = %s", e.code, e.msg)
}

// 声明一个错误
func NewCoder(code uint32, msg string) *ErrorCode {
    where := caller(1, false)
    return &ErrorCode{code: code, msg: msg, where: where}
}

// 对一个错误追加信息
func Wrap(err error, extMsg ...string) *ErrorCode {
    msg  := err.Error()
    if len(extMsg) != 0 {
        msg = strings.Join(extMsg, " : ") + " : " + msg
    }
    return &ErrorCode{msg: msg}
}

// 获取源代码行数
func caller(calldepth int, short bool) string {
    _, file, line, ok := runtime.Caller(calldepth + 1)
    if !ok {
        file = "???"
        line = 0
    } else if short {
        file = filepath.Base(file)
    }

    return fmt.Sprintf("%s:%d", file, line)
}

使用

// 子数据层
func InsertA(id int) (err error) {
    if id == 0 {
        // 非500, 则是"错误"
        err = error.NewCoder(400, "id不能为空")
        return
    }

    err := mysql.Insert(A{id:id})
    if err != nil {
        // 500则是"异常"
        err = error.NewCoder(500, err.String(), "插入数据库错误")
        return
    }
    return
}

func InsertB(id int) (err error) {
    if id == 0 {
        err = error.NewCoder(400, "id不能为空")
        return
    }
    return
}

// 数据层
func Main(aid int, bid int) (err error) {
    err = InsertA(aid)
    if err != nil {
      // 使用warp方法返回更详细的错
      err = error.Warp(err, "插入A错误")
      return
    }
    err = InsertB(aid)
    if err != nil{
      err = error.Warp(err, "插入B错误")
      return
    }

    return
}

// controll层
// 区分"错误"与"异常"并返回对应的响应
func Api(ctx *api.Content){
    var aid = 1
    var bid = 1
    err = Main(a, b)
    if err.Code() == 500 {
        // 将"异常"位置和信息都打印方便排除
        log.Error(err.Where(), err.String())
        ctx.response(500, "服务器错误")
        return
    }
    
    ctx.response(err.Code(), err.String())
}

最终的错误提示会是这样:

  • (错误) 插入A错误: id不能为空
  • (错误) 插入B错误: id不能为空
  • (异常) code.go:154 插入A错误: 插入数据库错误: sql: Scan error on column index 1: unsupported Scan, storing driver.Value type into type *string

emm 这样应该好排错吧

总结

当然这个方案并不完美,欢迎吐槽与讨论。

完整代码在这,还统一了gprc的错误处理:

package errors

import (
    "fmt"
    "strings"
    "google.golang.org/grpc/status"
    "google.golang.org/grpc/codes"
    "path/filepath"
    "runtime"
)

// 业务代码通用的错误
type ErrorCoder interface {
    Error() string
    Code() uint32
    Msg() string
    Where() string // 第一次生成这个错的地方, 第一次: 当newCoder和wrap一个非errorCoder的时候
}

// Grpc的错误
type GRPCStatuser interface {
    GRPCStatus() *status.Status
    Error() string
}

type ErrorCode struct {
    code  uint32
    msg   string
    where string
}

// 错误,附带code
func (e *ErrorCode) Error() string {
    return fmt.Sprintf("code = %d ; msg = %s", e.code, e.msg)
}

// 不带code的错误消息
func (e *ErrorCode) Msg() string {
    return e.msg
}

func (e *ErrorCode) Code() uint32 {
    return e.code
}
func (e *ErrorCode) Where() string {
    return e.where
}

func NewCoder(code uint32, msg string, extMsg ...string) *ErrorCode {
    if len(extMsg) != 0 {
        msg = strings.Join(extMsg, " : ") + " : " + msg
    }
    where := caller(1, false)
    return &ErrorCode{code: code, msg: msg, where: where}
}

func NewCoderWhere(code uint32, callDepth int, msg string, extMsg ...string) *ErrorCode {
    if len(extMsg) != 0 {
        msg = strings.Join(extMsg, " : ") + " : " + msg
    }
    where := caller(callDepth, false)
    return &ErrorCode{code: code, msg: msg, where: where}
}

func NewCodere(code uint32, err error, extMsg ...string) *ErrorCode {
    var msg string
    if err != nil {
        msg = err.Error()
    }
    if len(extMsg) != 0 {
        msg = strings.Join(extMsg, " : ") + " : " + msg
    }
    where := caller(1, false)
    return &ErrorCode{code: code, msg: msg, where: where}
}

// Wrap 为error添加一个说明, 当这个err不确定是否应该报500或者是由其他服务返回时使用
// 如果err是ErrorCoder或者GRPCStatuser, code将继承, 否则code为0
func Wrap(err error, extMsg ...string) *ErrorCode {
    var msg string
    var code uint32
    var where string
    switch v := err.(type) {
    case ErrorCoder:
        msg = v.Msg()
        code = v.Code()
        where = v.Where()
    case GRPCStatuser:
        s := v.GRPCStatus()
        if s.Code() == codes.Unknown {
            code = 0
        } else if s.Code() < 20 {
            // 只要是grpc自带的错误就说明是系统错误
            code = 500
        } else {
            code = uint32(s.Code())
        }
        msg = s.Message()
        where = caller(1, false)
    default:
        msg = v.Error()
        code = 0
        where = caller(1, false)
    }
    if len(extMsg) != 0 {
        msg = strings.Join(extMsg, " : ") + " : " + msg
    }
    return &ErrorCode{code: code, msg: msg, where: where}
}

func caller(calldepth int, short bool) string {
    _, file, line, ok := runtime.Caller(calldepth + 1)
    if !ok {
        file = "???"
        line = 0
    } else if short {
        file = filepath.Base(file)
    }

    return fmt.Sprintf("%s:%d", file, line)
}

func New(msg string) *ErrorCode {
    where := caller(1, false)
    return &ErrorCode{code: 0, msg: msg, where: where}
}


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:bysir

查看原文:Golang中Error处理方案

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

15122 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传