Go 之错误处理 | error

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

首先定义一个接口 WalkRun,它有两个方法 Walk 和 Run,如下面的代码所示:

type WalkRun interface {

   Walk()

   Run()

}
复制代码

现在就可以让结构体 person 实现这个接口了,如下所示:

func (p *person) Walk(){

   fmt.Printf("%s能走\n",p.name)

}

func (p *person) Run(){

   fmt.Printf("%s能跑\n",p.name)

}
复制代码

关键点在于,让接口的每个方法都实现,也就实现了这个接口。

提示:%s 是占位符,和 p.name 对应,也就是 p.name 的值,具体可以参考 fmt.Printf 函数的文档。

下面进行本节课的讲解。这节课我会带你学习 Go 语言的错误和异常,在我们编写程序的时候,可能会遇到一些问题,该怎么处理它们呢?

错误

在 Go 语言中,错误是可以预期的,并且不是非常严重,不会影响程序的运行。对于这类问题,可以用返回错误给调用者的方法,让调用者自己决定如何处理。

error 接口

在 Go 语言中,错误是通过内置的 error 接口表示的。它非常简单,只有一个 Error 方法用来返回具体的错误信息,如下面的代码所示:

type error interface {

   Error() string

}
复制代码

在下面的代码中,我演示了一个字符串转整数的例子:

func main() {

   i,err:=strconv.Atoi("a")

   if err!=nil {

      fmt.Println(err)

   }else {

      fmt.Println(i)

   }

}
复制代码

这里我故意使用了字符串 “a”,尝试把它转为整数。我们知道 “a” 是无法转为数字的,所以运行这段程序,会打印出如下错误信息:

strconv.Atoi: parsing "a": invalid syntax
复制代码

这个错误信息就是通过接口 error 返回的。我们来看关于函数 strconv.Atoi 的定义,如下所示:

func Atoi(s string) (int, error)
复制代码

一般而言,error 接口用于当方法或者函数执行遇到错误时进行返回,而且是第二个返回值。通过这种方式,可以让调用者自己根据错误信息决定如何进行下一步处理。

小提示:因为方法和函数基本上差不多,区别只在于有无接收者,所以以后当我称方法或函数,表达的是一个意思,不会把这两个名字都写出来。

error 工厂函数

除了可以使用其他函数,自己定义的函数也可以返回错误信息给调用者,如下面的代码所示:

func add(a,b int) (int,error){

   if a<0 || b<0 {

      return 0,errors.New("a或者b不能为负数")

   }else {

      return a+b,nil

   }

}
复制代码

add 函数会在 a 或者 b 任何一个为负数的情况下,返回一个错误信息,如果 a、b 都不为负数,错误信息部分会返回 nil,这也是常见的做法。所以调用者可以通过错误信息是否为 nil 进行判断。

下面的 add 函数示例,是使用 errors.New 这个工厂函数生成的错误信息,它接收一个字符串参数,返回一个 error 接口,这些在上节课的结构体和接口部分有过详细介绍,不再赘述。

sum,err:=add(-1,2)

if err!=nil {

   fmt.Println(err)

}else {

   fmt.Println(sum)

}
复制代码

自定义 error

你可能会想,上面采用工厂返回错误信息的方式只能传递一个字符串,也就是携带的信息只有字符串,如果想要携带更多信息(比如错误码信息)该怎么办呢?这个时候就需要自定义 error 。

自定义 error 其实就是先自定义一个新类型,比如结构体,然后让这个类型实现 error 接口,如下面的代码所示:

type commonError struct {

   errorCode int //错误码

   errorMsg string //错误信息

}

func (ce *commonError) Error() string{

   return ce.errorMsg

}
复制代码

有了自定义的 error,就可以使用它携带更多的信息,现在我改造上面的例子,返回刚刚自定义的 commonError,如下所示:

return 0, &commonError{

   errorCode: 1,

   errorMsg:  "a或者b不能为负数"}
复制代码

我通过字面量的方式创建一个 *commonError 返回,其中 errorCode 值为 1,errorMsg 值为 “a 或者 b 不能为负数”。

error 断言

有了自定义的 error,并且携带了更多的错误信息后,就可以使用这些信息了。你需要先把返回的 error 接口转换为自定义的错误类型,用到的知识是上节课的类型断言。

下面代码中的 err.(*commonError) 就是类型断言在 error 接口上的应用,也可以称为 error 断言。

sum, err := add(-1, 2)

if cm,ok:=err.(*commonError);ok{

   fmt.Println("错误代码为:",cm.errorCode,",错误信息为:",cm.errorMsg)

} else {

   fmt.Println(sum)

}
复制代码

如果返回的 ok 为 true,说明 error 断言成功,正确返回了 *commonError 类型的变量 cm,所以就可以像示例中一样使用变量 cm 的 errorCode 和 errorMsg 字段信息了。

错误嵌套

Error Wrapping

error 接口虽然比较简洁,但是功能也比较弱。想象一下,假如我们有这样的需求:基于一个存在的 error 再生成一个 error,需要怎么做呢?这就是错误嵌套。

这种需求是存在的,比如调用一个函数,返回了一个错误信息 error,在不想丢失这个 error 的情况下,又想添加一些额外信息返回新的 error。这时候,我们首先想到的应该是自定义一个 struct,如下面的代码所示:

type MyError struct {

    err error

    msg string

}
复制代码

这个结构体有两个字段,其中 error 类型的 err 字段用于存放已存在的 error,string 类型的 msg 字段用于存放新的错误信息,这种方式就是 error 的嵌套

现在让 MyError 这个 struct 实现 error 接口,然后在初始化 MyError 的时候传递存在的 error 和新的错误信息,如下面的代码所示:

func (e *MyError) Error() string {

    return e.err.Error() + e.msg

}

func main() {

    //err是一个存在的错误,可以从另外一个函数返回

    newErr := MyError{err, "数据上传问题"}

}
复制代码

这种方式可以满足我们的需求,但是非常烦琐,因为既要定义新的类型还要实现 error 接口。所以从 Go 语言 1.13 版本开始,Go 标准库新增了 Error Wrapping 功能,让我们可以基于一个存在的 error 生成新的 error,并且可以保留原 error 信息,如下面的代码所示:

e := errors.New("原始错误e")

w := fmt.Errorf("Wrap了一个错误:%w", e)

fmt.Println(w)
复制代码

Go 语言没有提供 Wrap 函数,而是扩展了 fmt.Errorf 函数,然后加了一个 %w,通过这种方式,便可以生成 wrapping error。

errors.Unwrap 函数

既然 error 可以包裹嵌套生成一个新的 error,那么也可以被解开,即通过 errors.Unwrap 函数得到被嵌套的 error。

Go 语言提供了 errors.Unwrap 用于获取被嵌套的 error,比如以上例子中的错误变量 w ,就可以对它进行 unwrap,获取被嵌套的原始错误 e。

下面我们运行以下代码:

fmt.Println(errors.Unwrap(w))
复制代码

可以看到这样的信息,即“原始错误 e”。

原始错误e
复制代码

errors.Is 函数

有了 Error Wrapping 后,你会发现原来用的判断两个 error 是不是同一个 error 的方法失效了,比如 Go 语言标准库经常用到的如下代码中的方式:

if err == os.ErrExist
复制代码

为什么会出现这种情况呢?由于 Go 语言的 Error Wrapping 功能,令人不知道返回的 err 是否被嵌套,又嵌套了几层?

于是 Go 语言为我们提供了 errors.Is 函数,用来判断两个 error 是否是同一个,如下所示:

func Is(err, target error) bool
复制代码

以上就是errors.Is 函数的定义,可以解释为:

  • 如果 err 和 target 是同一个,那么返回 true。
  • 如果 err 是一个 wrapping error,target 也包含在这个嵌套 error 链中的话,也返回 true。

可以简单地概括为,两个 error 相等或 err 包含 target 的情况下返回 true,其余返回 false。我们可以用上面的示例判断错误 w 中是否包含错误 e,试试运行下面的代码,来看打印的结果是不是 true。

fmt.Println(errors.Is(w,e))
复制代码

errors.As 函数

同样的原因,有了 error 嵌套后,error 断言也不能用了,因为你不知道一个 error 是否被嵌套,又嵌套了几层。所以 Go 语言为解决这个问题提供了 errors.As 函数,比如前面 error 断言的例子,可以使用 errors.As 函数重写,效果是一样的,如下面的代码所示:

var cm *commonError

if errors.As(err,&cm){

   fmt.Println("错误代码为:",cm.errorCode,",错误信息为:",cm.errorMsg)

} else {

   fmt.Println(sum)

}
复制代码

所以在 Go 语言提供的 Error Wrapping 能力下,我们写的代码要尽可能地使用 Is、As 这些函数做判断和转换。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享