14

Golang error浅析

 3 years ago
source link: https://studygolang.com/articles/31845
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

由于Golang的语言设计的原因,不管是不是愿意,每个golang开发者的几乎每一段代码都需要与error做缠斗。下面我就简单分析一下golang中的error相关。

error是什么?

首先需要明确的一点是,golang中对于 error 类型的定义是什么?不同于很多语言的 exception 机制, golang 在语言层面经常需要显示的做错误处理。其实从本质上来讲, golang 中的 error 就是一个接口:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

和所有接口含义一样, nil 表示零值。

before Go1.13

在golang的1.13版本之前,官方给到的错误处理方法寥寥无几,只有用来构造无额外参数的错误的 errors.New 和构造带额外参数的错误的 fmt.Errorf 。当时,经常需要使用标准库之外的扩展库来支持更丰富发错误构造和处理,比如由Dave Cheney主导的 github.com/pkg/errors

这些额外的error库主要的关注点在于提供方法用于描述错误的层级。回到上面的错误本身的定义,只是一个包含 Error 方法的接口,本身缺乏对于类似其他语言中类似 traceback 的描述能力,无法追踪错误的详细栈信息。

而以 github.com/pkg/errors 为代表的库,通过实现 WrapCause 方法对来提供了包装/拆包错误的能力,提供了类似 traceback (但需要开发者自己定义额外信息)和逐层解析并比较错误的能力。通过这个方法对,我们可以实现下面的用例:

// 为错误提供更丰富的上下文信息,方便定位错误
if _, err := ioutil.ReadAll(r);err != nil {
        return errors.Wrap(err, "read file failed")
}

// 判断错误的根错误是什么,根据最初的错误类型判断需要走什么错误处理逻辑
switch err := errors.Cause(err).(type) {
case *io.EOF:
        // handle specifically
default:
        // unknown error
}

After Go1.13

对于上面描述的错误处理,相比于较为成熟的 exception 处理模式,天生缺乏错误栈信息的缺点让很多开发者非常不满,虽然第三方库或多或少的弥补了这个缺点,但是作为开发中占比非常大的一部分代码,官方库的缺乏支持还是令人不满。所以Go team在1.13版本中进一步完善了错误相关的官方库支持。

首先,提供了 %w 构造方法和 errors.Unwrap 的方法对来支持类似 WrapCause 相关的能力。

// 为错误提供更丰富的上下文信息,方便定位错误
if _, err := ioutil.ReadAll(r);err != nil {
        return fmt.Errorf("read file failed with err:%w", err)
}

// 判断错误的根错误是什么,根据最初的错误类型判断需要走什么错误处理逻辑
rawErr := errors.Unwrap(err)

不仅如此,官方库还带来了两个错误比较相关的API:

if errors.Is(err, io.EOF){
    ...
}

var eof io.EOF
if errors.As(err, &eof){
    ...
}

其中, errors.Is 方法会逐层调用 Unwrap 方法,去和目标 err 做比较,知道没有 Unwrap 方法或者 err 比较成功。 errors.As 方法的作用类似于之前的针对错误的类型断言。

至此, golang 官方库提供了错误的构造方法,错误的比较方法,额外信息包装的能力,总体来说应该算是比较完善了。

关于Go1.13错误处理相关的实现,可以 参考

夭折的 try

另外一个小小的番外插曲,曾经有一个呼声颇高的错误处理相关的提案:引入 try 关键字来增强错误处理的能力。主要使用方法如下:

// 包装调用方法
readFile := try(ioutil.ReadAll(r))
...
// 函数层级统一
defer func(){
    if err!=nil{
        switch err.(type){
            ...
        }  
    }
}()

带来的便利是减少了大量的 if err!=nil 语句,提供函数层级的统一错误处理处(一般在defer处)。然而最后由于可读性和显式处理错误的种种原因,这个提案被拒绝了。

更近一步的信息可以参考 github上相关的讨论设计文档

实践

基于go1.13提出的现有错误处理工具,我们大概能够采用下面的实践来进行错误处理:

  1. 针对基础错误类型,一般通过直接声明变量或者自定义结构:
// 常规的无额外参数的error
var BasicErr1 = errors.New("this is a basic error.")

func fn() error{
    ...
    if conditionA{
        return BasicErr
    }
}

// 调用处
if err!=nil{
    if errors.Is(err, BasicErr1){
        ...
    }
}

// 带参数信息的错误
type CustomErr struct {
    Code int64
    Msg string
}

func (e CustomErr)Error() string{
    return fmt.Sprintf("%d:%s", e.Code, e.Msg)
}

func fn() error{
    ...
    if conditionA{
        return CustomErr{Code: 123, Msg: "test"}
    }
}

// 调用处
if err!=nil{
    if e,ok:=err.(CustomErr);ok{
        ...
    }
}
  1. 对于调用三方库获取的报错,一般将额外信息(比如调用参数,上下文信息等方便定位问题的信息)包装之后向上层调用方直接抛出:
if _,err:=ioutil.ReadAll(r);err!=nil{
    return fmt.Errorf("read file failed:%w", err)
}

// 调用方
if err!=nil{
    if errors.Is(err, io.EOF){
        ...
    }
}

关于错误日志的处理部分,为了防止处处打日志造成的上下文信息分散和大量信息冗余,一般建议的处理方式是对于内部方法的调用,使用 %w 包装错误和必要的额外信息,直接返回到上层;对于最外层方法(一般是http handler或者rpc handler),将错误包装上下文,打印到错误日志中,再使用 errors.Is 或者 errors.As 方法,根据错误类型进行不同的错误处理逻辑。这样的好处是,对于全局而言,有且只有最外层一份错误日志,而这个错误信息时包装了层层调用信息的,内容最为齐全。

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

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK