15-错误处理

error类型是一个接口类型,是Go语言內建类型,在这个接口类型的声明中只包含一个方法Error,这个方法不接受任何参数,但是会返回一个string类型的结果。

type error interface {
        Error() string      //返回错误信息的字符串形式
}

使用error类型的方式,通常是在函数声明的结果列表的最后,声明一个error类型的结果,同时在调用这个函数之后,先判断它返回的最后一个结果值是否“不为nil”。

package main

import (
 "errors"
 "fmt"
)

func echo(request string) (response string, err error) {
 if request == "" {                  // 卫述语句
  err = errors.New("empty request")
  return
 }
 response = fmt.Sprintf("echo: %s", request)
 return
}

func main() {
 for _, req := range []string{"", "hello!"} {
  fmt.Printf("request: %s\n", req)
  resp, err := echo(req)
  if err != nil {                 // 卫述语句
   fmt.Printf("error: %s\n", err)
   continue
  }
  fmt.Printf("response: %s\n", resp)
 }
}

注意点:

  1. 在进行错误处理的时候,经常会用到卫述语句
  2. 在生成error类型值的时候用到errors.New函数,这是一种最基本的生成错误值的方式。调用它时传入一个有字符串代表的错误信息,返回一个包含这个错误信息的error类型值。该值的静态类型是error,动态类型是一个errors包中,包级私有的类型*errorString

errorString类型拥有的一个指针方法实现了errors接口中的Error方法。这个方法被调用后,会原封不动地返回之前传入的错误信息,实际上,error类型值的Error方法就相当于其他类型值的String方法。

在上述例子中,fmt.Printf函数发现被打印的是一个error类型,就会调用它的Error方法。在fmt包中,这类打印函数其实都是这么做的。

当我们通过模板化的方式生成错误信息,并得到错误值时,可以使用fmt.Errorf函数,该函数其实是先调用fmt.Sprintf函数,得到确切的错误信息,在调用errors.New函数,得到该错误信息的error类型值,最后返回该值。

如何判断错误值具体代表哪一类错误

因为error是一个接口类型,所以即使同为error类型的错误值,他们的实际类型也可能不同。

  1. 对于类型在已知范围内的一系列错误值在:一般使用类型断言表达式或者switch语句来判断
  2. 对于已有相应变量且类型相同的一些错误值,一般直接使用判等操作来判断
  3. 对于没有相应变量且类型未知的一系列错误值:只能使用错误信息的字符串表示形式来判断。

类型在已知范围内

类型在已知范围内的错误值是最容易分辨的。如os包中的几个代表错误类型:

  • os.PathError
  • os.LinkError
  • os.SyccallError
  • os/exec.Error

它们的指针类型都是error接口的实现类型,同时它们也都包含了一个名叫Err,类型为error接口类型的代表潜在错误的字段。如果得到一个error类型值,并且知道该值的实际类型肯定是它们中的某一个,那么就用switch语句去判断:

func underlyingError(err error) error {
 switch err := err.(type) {
 case *os.PathError:
  return err.Err
 case *os.LinkError:
  return err.Err
 case *os.SyscallError:
  return err.Err
 case *exec.Error:
  return err.Err
 }
 return err
}
// 只要类型不同就可以使用这种方式来判断但是如果错误值类型相同那么这种方式就无效了

已有相应变量且类型相同

如os包中不少错误类型都是通过调用errors.New函数来初始化:

  • os.ErrClosed
  • os.ErrInvalid
  • os.ErrPermission

这几个都是已经定义好的,确切的错误值。os包中的代码有时候会把他们当做潜在的错误值,封装进前面那些错误类型的值中

如果在操作文件系统的时候得到一个错误值,并且知道该值的潜在错误值肯定是上述值中的某一个,那就可以用普通的switch或者if和判等语句去做判断:

printError := func(i int, err error) {              // 接受error类型的参数值,该值代表某个文件操作相关的错误
 if err == nil {
  fmt.Println("nil error")
  return
 }
 err = underlyingError(err)
 switch err {
 case os.ErrClosed:
  fmt.Printf("error(closed)[%d]: %s\n", i, err)
 case os.ErrInvalid:
  fmt.Printf("error(invalid)[%d]: %s\n", i, err)
 case os.ErrPermission:
  fmt.Printf("error(permission)[%d]: %s\n", i, err)
 }
}
// 虽然不知道这些错误值的类型范围但却知道它们或它们潜在的错误值一点在某个已知的os包中定义的值

没有相应变量且类型未知

如果对于一个错误值可能代表的含义知之甚少,那么只能通过它拥有的错误信息去判断了

我们总是能通过错误值的Error方法拿到它的错误信息。在os包中有os.IsExitos.IsNotExitos.IsPermission函数来判断。

如何根据实际情况给予恰当的错误值

构建错误值体系的基本方式有两种:

  1. 创建立体的错误类型体系
  2. 创建扁平的错误值列表

错误类型体系

在Go语言中实现接口都是非侵入式的,所以可以做的非常灵活。

比如在net包中,有一个名为Error的接口类型,它算是內建接口类型error的一个扩展接口,因为error是net.Error的嵌入接口。

type Error interface {
        error
        Timeout() bool   // Is the error a timeout?
        Temporary() bool // Is the error temporary?
}

net包中有很多错误类型都实现了net.Error接口,如:

  • *net.OpError
  • *net.AddrError
  • net.UnknownNetworkError

image

把错误类型想象成一棵树,内建接口error就是树根,net.Error接口就是一个在根上延伸的第一级非叶子节点。

用类型建立起树形结构的错误体系,用统一字段建立起可追根溯源的链式错误关联。

如果不希望包外代码改动返回错误值的话,一定要小写其中字段的名称首字母。通过暴露某些方法让包外代码有进一步获取错误信息的权限,比如编写一个可以返回包级私有的err字段值的公开方法Err。

错误列表值

相对于立体的错误类型体系,扁平的错误列表值简单很多。只是想要先创建一些代表已知错误的错误值的时候,用扁平化的方式很恰当。

上次修改: 25 November 2019