目录

Go error处理

推荐使用github.com/pkg/errors,自带堆栈信息

error

error是接口,errors.New()返回的是地址

error是一个接口,需要实现Error() string方法
errors是一个标准库,定义了errorString结构体,其只有一个string字段,实现了error接口的Error() string方法,返回字符串;另外提供了New方法:

func New(text string) error {
  return &errorString{text}
}

注意,返回的是结构体地址,因为用户可能会定义一个和标准库中的errorString底层结构相同的结构体,把这个结构体强转为errorString后可与之进行比较,若值相同则有可能相同,而这两者是不同的类型,故应该是不同的错误,所以为避免这种情况,返回的是地址;另外返回地址也避免了临时变量的拷贝

error判断的时候,要判断失败的时候,不要判断正常的时候

因go具有多返回值的特性,所以error多都伴随返回值一起返回
注意,当返回值中有error信息时,一定要先判断error再使用value

//推荐做法,更简洁
if err != nil {
  //...错误处理...
}
//...正常逻辑...


//不推荐做法,混乱
if err == nil {
  //...正常逻辑...
} else {
  //...错误处理...
}

sentinel error

主要用于输出到日志、输出到stdout,但不适用于错误判断

/images/go/err1.png

弊端:

/images/go/err2.png

error type

即自定义的error类型,尽量不用

/images/go/err3.png

附:其中类型断言为

var x interface{}
x = 10
value, ok := x.(int)

类型switch为

switch x.(type){
    case type:
       statement(s);      
    case type:
       statement(s); 
    default: /* 可选 */
       statement(s);
}

推荐做法:不透明的错误处理(opaque error)

这种方式使得代码和调用者之间的耦合最少
不透明是因为看不到错误内部,只知道发生了错误,只关注这个结果
总结:只需返回错误而不假设其内容

wrap error

前言:通常系统都会有日志,而错误一般都要记录到日志中去,所以错误的正确处理应该是如何将错误简洁地记录到日志,并能够准确定位到错误

第一种方式: /images/go/err4.png

优:只在最上层打印日志,简洁
缺:这种方式从下一直抛到上,error没有附带任何上下文信息,所以error非常难定位

第二种方式: /images/go/err5.png

优:只在最上层打印日志,简洁
缺:这种方式记录了一定的上下文信息,但只有报错信息,而没有堆栈的文件和行号信息,且先后的错误都放在一行,定位错误较难

第二种方式的变体: /images/go/err6.png

缺:日志多次打印,繁琐,而且系统一般都是多线程,日志输出会交叉,那么在上下文的两次错误日志中会插入其他日志,使得错误难以追踪

由此可见:错误处理准则:错误只应该被处理一次,即要不向上层抛,要不在本层处理且记录日志;所以一般都是底层只向上层抛wrap的error,而上层打印日志
不要在本层既记录日志又向上层抛,否则日志异常臃肿

第三种方式:errors.Wrap()
使用github.com/pkg/errors包中的Wrap()方法

只在顶层打印日志,底层错误进行Wrap,记录堆栈信息
优:简洁(只在最上层打印日志)且容易定位(具有堆栈信息)

使用wrap error的注意事项

/images/go/err7.png
/images/go/err8.png

  1. 自己写的函数发生错误,返回新建错误
  2. 调用自己本包中的函数发生错误,返回wrap error
  3. 调用自己其他包中的函数发生错误,直接返回被调函数返回的error
  4. 但若调用的是标准库、自己的基础库、第三方如github库时,返回wrap error
  5. 上层应用使用wrap error,而高可重用的包只返回根错误值,比如像sql.ErrNoRows之类的基础(kit)库

**总结:**只有调用自己其他包中的函数发生错误时才直接返回被调函数返回的error,否则都返回wrap error
Wrap只需在第一次出现的地方(层)wrap一次,因为wrap后保存的堆栈是从上到下所有层的信息,所以无需每层都进行wrap,上层一般进行日志记录,中间层一般进行上抛
基础库一般不会直接 error.wrap,即基础库(kit)一般暴露原始错误,大多由外部业务代码进行wrap,否则很容易嵌套 wrap,也不合理。
如果只是自已使用的,然后第一次出现的错误,那就进行 warp,如果是给别人使用的,就直接抛出底层错误,然后给调用方使用的时候,去决定要不要 warp。
用行为来定义错误,有待理解

panic 与 exception

Go中有panic机制,但和其他语言如c++中的exception时不同的
exception是当前函数抛出异常,交由调用者进行处理,并且相信其可以妥善处理
panic时意味着fatal error,就是程序挂掉,代码不能继续运行了,不能假设调用者来解决panic,即当发生了处理不了的错误,代码不能再正常地继续执行下去时,就进行panic

野生go routine发送panic会导致程序挂掉,即如果一个go routine发送panic,在主程序是recover不住的,如

//...主程序中起了一个go routine...
go func() {
  panic("panic occur")
  fmt.Println("ok")
}()
//go routine 发生 panic导致主程序panic

解决方式1:规范程序员在go routine中recover

go func() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("recover")
    }
  }()
  panic("panic occur")
  fmt.Println("ok")
}()

解决方式二:方式一中的go routine代码冗长,且程序员也不一定保证,所以可考虑封装一个函数,如下:

func Go(f func()) {
  defer func() {
    if err := recover(); err != nil {
      //panic处理
    }
  }()

  f()
}

在主程序中这样执行,即可避免野生go routine发送panic

go Go(myFunc)

这个封装最好放到一个包中,或封装一个线程池

panic使用场景

  1. 对于不可恢复的错误:索引越界、栈溢出等
  2. 初始化强依赖的服务或配置时,如果不成功,则可以panic

业务逻辑不要用panic