错误处理

2023/1/15

# error类型定义

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。 error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

# 构造错误值的方法

两种方便 Go 开发者构造错误值的方法: errors.New和fmt.Errorf

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

errors.New示例:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

result, err:= Sqrt(-1)
if err != nil {
   fmt.Println(err)
}

我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用以输出错误

大多数情况下,使用这两种方法构建的错误值就可以满足我们的需求了。但我们也要看到,虽然这两种构建错误值的方法很方便,但它们给错误处理者提供的错误上下文(Error Context)只限于以字符串形式呈现的信息,也就是 Error 方法返回的信息。

但在一些场景下,错误处理者需要从错误值中提取出更多信息,帮助他选择错误处理路径,显然这两种方法就不能满足了。这个时候,我们可以自定义错误类型来满足这一需求。

# 自定义错误类型

比如:标准库中的 net 包就定义了一种携带额外错误上下文的错误类型:

// $GOROOT/src/net/net.go
type OpError struct {
    Op string
    Net string
    Source Addr
    Addr Addr
    Err error
}

这样,错误处理者就可以根据这个类型的错误值提供的额外上下文信息,比如 Op、Net、Source 等,做出错误处理路径的选择,比如下面标准库中的代码:

// $GOROOT/src/net/http/server.go
func isCommonNetReadError(err error) bool {
    if err == io.EOF {
        return true
    }
    if neterr, ok := err.(net.Error); ok && neterr.Timeout() {  
        return true
    }
    if oe, ok := err.(*net.OpError); ok && oe.Op == "read" {  // 这里
        return true 
    }
    return false
}

上面这段代码利用类型断言,判断 error 类型变量 err 的动态类型是否为 *net.OpError 或 net.Error。如果 err 的动态类型是 *net.OpError,那么类型断言就会返回这个动态类型的值(存储在 oe 中),代码就可以通过判断它的 Op 字段是否为"read"来判断它是否为 CommonNetRead 类型的错误。

# 常见几种错误处理的惯用策略

# 策略一:透明错误处理策略

# 策略二:“哨兵”错误处理策略

# 各语言错误处理对比

# C语言错误处理

C 语言中,我们通常用一个类型为整型的函数返回值作为错误状态标识,函数调用者会基于值比较的方式,对这一代表错误状态的返回值进行检视。通常,这个返回值为 0,就代表函数调用成功;如果这个返回值是其它值,那就代表函数调用出现错误。也就是说,函数调用者需要根据这个返回值代表的错误状态,来决定后续执行哪条错误处理路径上的代码。 C 语言的这种简单的、基于错误值比较的错误处理机制有什么优点呢?

  • 首先,它让每个开发人员必须显式地去关注和处理每个错误,经过显式错误处理的代码会更健壮,也会让开发人员对这些代码更有信心。
  • 另外,这些错误就是普通的值,所以我们不需要用额外的语言机制去处理它们,我们只需利用已有的语言机制,像处理其他普通类型值一样的去处理错误就可以了,这也让代码更容易调试,更容易针对每个错误处理的决策分支进行测试覆盖。

C 语言这种错误处理机制也有一些弊端:

  • 比如,由于 C 语言中的函数最多仅支持一个返回值,很多开发者会把这单一的返回值“一值多用”。什么意思呢?就是说,一个返回值,不仅要承载函数要返回给调用者的信息,又要承载函数调用的最终错误状态。比如 C 标准库中的fprintf函数的返回值就承载了两种含义。在正常情况下,它的返回值表示输出到 FILE 流中的字符数量,但如果出现错误,这个返回值就变成了一个负数,代表具体的错误值:
// stdio.h
int fprintf(FILE * restrict stream, const char * restrict format, ...);

特别是当返回值为其他类型,比如字符串的时候,我们还很难将它与错误状态融合到一起。这个时候,很多 C 开发人员要么使用输出参数,承载要返回给调用者的信息,要么自定义一个包含返回信息与错误状态的结构体,作为返回值类型。大家做法不一,就很难形成统一的错误处理策略。

# Go语言错误处理

Go语言使用 error 类型,而不是传统意义上的整型或其他类型作为错误类型的好处

  • 第一点:统一了错误类型。
    如果不同开发者的代码、不同项目中的代码,甚至标准库中的代码,都统一以 error 接口变量的形式呈现错误类型,就能在提升代码可读性的同时,还更容易形成统一的错误处理策略。
  • 第二点:错误是值。 我们构造的错误都是值,也就是说,即便赋值给 error 这个接口类型变量,我们也可以像整型值那样对错误做“==”和“!=”的逻辑比较,函数调用者检视错误时的体验保持不变。
  • 第三点:易扩展,支持自定义错误上下文。
    虽然错误以 error 接口变量的形式统一呈现,但我们很容易通过自定义错误类型来扩展我们的错误上下文,就像前面的 Go 标准库的 OpError 类型那样。error 接口是错误值的提供者与错误值的检视者之间的契约。error 接口的实现者负责提供错误上下文,供负责错误处理的代码使用。这种错误具体上下文与作为错误值类型的 error 接口类型的解耦,也体现了 Go 组合设计哲学中“正交”的理念。