Golang 错误声明的几种方式

golang的错误处理机制非常特别,golang将错误声明为一种接口,并且可以作为参数传递。这种机制是为了让程序员重视错误处理,同时避免胡乱引发异常的情况。

在使用golang返回错误的时候,我通常使用以下几种方式:

1.直接返回声明的错误:

示例:

return errors.New("这是一个错误!")

return fmt.Errorf("这是一个错误!")

这种方式使用起来是最快捷的,同时也是我最不推荐的。因为这种方式上级函数拿到错误之后很难分析到底发生了什么错误,即使可以通过比对字符串进行分析,也是既低效又不稳定的方法。

2.返回声明的错误变量

示例:

var ErrMyErr  error = errors.New("这是一个错误!")

fun returnErr() err {
    return ErrMyErr
}

这种方式是我最推荐的方式,因为这是声明最方便,同时也是判断最简单的方案。上级函数拿到错误之后只需要简单的使用 == 运算就可以判断错误类型。

if err == ErrMyErr {
    // 进行错误处理
}

3.声明一种错误结构体:

示例:

type ErrMyErr struct {
    ErrString string
}

func (e ErrMyErr) Error() string {
    return fmt.Sprintf("字符串%s发生了错误", e.ErrString)
}

// 返回错误的函数
func returnErr()  error {
    return ErrMyErr{"发生错误的字符串"}
}

如果你的错误比较简单,只需要返回一个错误变量就可以让上级函数轻松的判断具体发生了什么错误,但是如果你的错误比较复杂,有一些参数需要跟随错误上报,那么简单的变量就不能满足这种情况了,我们需要先声明一种错误结构体,表示一类错误,然后声明独特的错误实例。这种情况适用于这一类错误每次发生的情况都会有一些小的区别的时候,比如查询数据库中的行的时候,某一列不存在,需要通过返回错误告知。

这种情况下判断错误类型也很简单:

if _, ok := err.(ErrMyErr); ok {
    // 错误处理
}

4.声明一种错误结构体,同时通过属性来区别错误。

示例:

type ErrMyErr struct {
    ErrString string
    ErrType uint
}

func (e ErrMyErr) Error() string {
    return fmt.Sprintf("字符串%s发生了错误", e.ErrString)
}

const (
    ErrTypezero    uint =  iota
    ErrTypeone
    //....
)

// 返回错误的函数
func returnErr() error {
    return ErrMyErr{"发生错误的字符串", ErrTypezero}
}

这种返回错误的方式就比较复杂了,幸亏它的适用场景也很少,大多数情况下,最好不要使用这种返回错误的方式。这种错误的定义方式适用于那些存在很多种类的错误,同时又是一大类错误的时候,有的时候我们只需要知道发生的错误是不是这一个大类,有的时候我们又希望知道具体发生的错误是哪个小类。当然我们也可以继续使用上一种错误声明方式,但是可能又要面临第一中错误声明方式遇到的比对字符串的情况。所以不如直接在结构体中定义一个属性用于说明错误的小类。

判断方法:

if _, ok := err.(ErrMyErr); ok {
    // 错误处理
}
if e, ok := err.(ErrMyErr); ok && e.ErrType == ErrTypezero {
    // 错误处理
}

5.使用普通的变量代表错误

以上几种方式都是围绕golang的错误处理机制实现的,但是其实仔细一想,这种错误处理机制并不受限于golang的语法,只是一种大家共同认可的方式,如果你自己简单的声明一个变量代表一种错误,同样可以实现返回错误。但是为了代码的兼容性,我也完全不赞成在错误处理的时候使用这种方式,除非你的错误非常小,或者算不上什么错误,别人根本不会在乎,也完全不需要实现Error()函数。

示例:

func returnErr() int {
    return 0
}

这几种方式是我能想到的几种声明错误的方式,我推荐先在包中定义错误变量,然后直接返回错误变量,这种方式几乎适用于绝大多数的情况,而且处理起来非常方便稳定。为了代码的兼容性和可维护性,希望看到这篇文章的人也都能养成良好的错误声明的习惯。

6.最近我在阅读别人的代码时发现了另外一种定义错误的方式,就是把字符串重定义为错误类型,然后直接将错误描述的字符串强制类型转换为错误类型返回。

type Err String
func (e Err) Error() string{return string(e)}
func returnErr() error {
    return Err{"这里发生了一个错误"}
}

你也许会觉得,这和第一种方式没有什么区别,但是实际上相比第一种方式,这种方式多了一个好处,在使用时可以判断这个错误是否你调用的模块发生的错误。通常,我们调用的模块都会有依赖模块,比如我们用的orm会调用数据库的模块,当数据库的模块发生错误的时候orm通常会直接把错误传递给我们,如果ORMorm块在定义错误的时候采用的是这种方式,那么我们就可以判断得到的错误是不是orm.Err类型,以了解到这个错误到底是在orm模块发生的还是在数据库模块发生的,而且返回错误的过程更加简单。

7.使用Wrap方法定义错误

之前我使用errors包的时候仅仅使用New函数,除此之外,errors包还提供了Unwrap Is As 这三个函数。这就为golang简陋的错误机制稍稍带来了一点点提升。那我们还是尽快用起来吧。

要解释Unwrap函数首先要解释golang提供的Wrap机制,原来我们基于一个错误生成新的错误,通常是使用格式化工具叫原本的错误转成字符串插入到新的错误中。

比如:

oldError := errors.New("旧的错误")
newError := fmt.Errorf("发生了一个错误:%v", oldError)

这样带来的一个问题是原始的错误转换为字符串之后丢失了大量信息,原来的错误没法从新的错误里解析出来,也就没法用比较或者断言的方式判断原来发生的是哪个错误。

能想到的一个最高的方法就是把原来的错误原封不动的保存在新的错误中,需要的时候将原来的错误解析出来。但是golang没有提供Wrap函数,仅仅提供了Unwrap函数,Wrap函数需要我们自己实现。

那么我们可以定义一个错误类型为:

type TheNewError struct {
  oldError error
  errString string
}

func (e TheNewError )Wrap(err error) error {
  e.oldError = err
  return e
}

func (e TheNewError  )Unwrap() error {
  return e.oldError
}

func (e TheNewError  )Error() string {
  return fmt.Sprintf("%v: %v", e.errString,  e.oldError)
}

当需要返回错误的时候执行:

err := execSomeThing()
if err != nil {
  return TheNewError{}.Wrap(err)
}

如果需要获取TheNewError中嵌入的错误,使用errors.Unwrap函数就可以了。errors.Unwrap函数会调用TheNewError的Unwrap函数获取返回值。

使用Wrap机制处理错误会导致原来的使用相等运算和类型判断的方法一定程度上失效,因为返回的错误可以已经被Wrap了。

为此,golang提供了Is和As两个函数来解决这个问题。

Is(err, target error) bool 函数用于判断两个错误是否是同样的错误,Is函数会层次Unwrap 第一个参数并和第二个参数对比,如果存在相等的错误则返回true,否则返回false,这样就可以对Wrap过的错误进行比较。

比如:

var err = errors.New("原始的错误")
wrapedError := TheNewError{}.Wrap(err)
if errors.Is(wrapedError, err) {
 fmt.Printf("%v is %v", wrapedError, err)
}

As(err error, target interface{}) bool 函数用于获取第一个参数中嵌入的类型与target相同的错误,并将该错误解析到target中,如果这个错误不存在,则返回false。所以target必须是指针类型,这样As函数才能进行赋值。

比如:

type TheOldError struct {
  String string
}
wrapedError:= execSomeThing()
target := TheOldError{}
if errors.As(wrapedError, &target) {
  fmt.Printf("%v is wraped from %v", wrapedError, target)
}

虽然golang没有像内建error接口那样内建一个wrapedError,并且我们自己实现Wrap函数似乎也不错。但是还是有更简单方便的用法的。fmt.Errorf 实际上也是支持Wrap机制的。

我们可以使用:

err := execSomeThing()
wrapedError := fmt.Errorf("发生了一个错误: %w", err)

快速创建一个wrapedError,格式化字符串%w就表示一个需要嵌入的错误。

既然golang已经原生支持了wrapedError,我推荐大家还是用起来吧,有总比没有好嘛。随手定义错误虽然开发起来简单,但是会给后续的维护者增添大量负担,并且不利于代码的复用。