错误处理是使用任何编程语言都需要考虑的一个重要因素,因为软件在执行过程中,难免发生错误。许多编程语言在发生错误或捕获错误时抛出异常,Go 语言却提供了一种有趣的方式处理错误:它将错误定义为一种类型。这样可以将错误通过函数或方法传递。Go 语言处理错误的思路是:通过最后一个返回值将错误返回给调用者,由调用者决定如何处理错误。Go 语言还引入了三个关键字用于标准的错误处理流程,这三个关键字分别是 defer、panic、recover。

作为有经验的程序员,要尽量考虑程序可能发生的错误,并提前编写好处理错误的代码。就像你准备面试时,会提前考虑风险,这样即使公共汽车出现了故障,也不会影响到你准时参加面试,因为你已提前做了准备。

总之,错误处理是编写健壮、易维护且可靠代码的重要组成部分。如果独力开发项目,它可以帮助你处理意外情况;如果编写开源项目,错误处理是创建值得信赖的代码的一部分。

9.1 错误类型

Go 语言中的错误是一个接口类型,声明如下:

type error interface {
  Error() string
}

error 接口提供了一个名为 Error 的方法,方法返回一个字符串类型。我们看看 Error 方法是如何定义的:

package errors

type errorString struct {
    text string
}

func New(text string) error {
    return &errorString(text)
}

func (e *errorString) Error() string {
    return e.text
}
package fmt

import "errors"

func Errorf(format string, args ...interface{}) error {
    return errors.New(Sprintf(format, args...))
}

9.2 创建 error

通过以上源码,可知创建 error 的两种方式:

err1 := errors.New("This is err1")

//格式化错误
err2 := fmt.Errorf("%s", "This is err2")

9.3 错误处理

当调用方法或函数失败时,Go 语言的规范之一是将错误类型作为最后一个值返回。也就是说,如果函数调用报错,错误不会在函数内引发任何异常,而是将异常返回给调用者,由调用者决定如何处理错误。如下所示,当错误发生时,ioutil.Readfile 函数会返回错误:

package main

import (
    "fmt"
    "io/ioutil"
)

func main()  {
    file, err := ioutil.ReadFile("file.txt")

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%s", file)
}

虽然代码量很少,但它表达了 Go 语言处理错误的思想。

代码释义如下:

  • 使用 io/ioutil 标准包中的 ReadFile 函数读取文件;

  • 如果 err 不为 nil,说明有错误发生

  • 打印 err,并退出程序

  • 如果 err 为 nil,打印文件内容

ReadFile 函数定义如下:

func ReadFile(filename string) ([]byte, error)

它接受一个字符串类型的参数,并返回一个字节切片和一个 error 类型,理解这些,有助于我们了解 Go 语言是如何处理错误的。

调用 ReadFile 函数总能收到一个 error 类型的值,本例 main 函数的第一行,ReadFile 函数的返回值赋给了变量 file 和 err,这是 Go 语言的常见写法。在其他开源项目中,你也可能会看到这些写法:

var file []byte
var err error
file, err = ioutil.ReadFile("files.txt")

如果在执行中没有发生错误,则 error 值为 nil。我们可以在调用方法或函数时,检查 error 类型的值,来判断程序是否按预期执行。

if err != nil {
    // something went wrong.
}

这种写法在 Go 语言中很常见,可能会有些开发者觉得这种方式过于繁琐,因为每次调用方法或函数时都需要检查错误。确实如此,但在处理错误方面,Go 语言仍然比其他语言提供了更多的灵活性,因为错误可以像其他类型一样在函数中传递,这可以帮我们显著缩短代码量。

9.4 从函数返回 error

在本章不止一次提到,Go 语言处理错误的思想是将错误作为函数或方法的最后一个返回值,这个示例,介绍下如何创建和返回错误:

package main

import "fmt"

func Division(x, y int) (int, error) {
    if y == 0 {
        return -1, fmt.Errorf("The divisor %d cannot be 0", y)
    }

    return x / y, nil
}

func main()  {
    n, err := Division(16, 0)

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(n)
}

示例中定义了一个返回 error 类型的函数 Division,以及调用者是如何处理错误的。代码释义如下:

  • 整数16和0传递给 Division 函数;

  • Division 函数使用比较运算符,检查0是否等于0;

  • Division 函数返回-1和 error 类型的值;

  • 调用者检查到 err 不为 nil,输出错误并执行 return 语句。

这个示例体现了 Go 语言的错误处理思想:错误处理不是在函数中发生,而是在调用函数的位置。这使得在处理错误时具有更大的灵活性,而不是采用一刀切的方法。

9.5 错误和可用性

在定义错误提示信息时,一定要以用户为中心,因为精确的错误能有效提高程序可用性。其他人可能引用你开发的代码,并希望在遇到错误时,能快速从中恢复。考虑以下错误,根据该提示,你认为这能让你轻松处理或从错误中恢复吗?

Ha Ha Ha, This is error!请自求多福。

很显然,这是一个很糟糕的错误消息,因为它没有提供错误发生的线索,也没有提供如何恢复该错误的建议。再看一个正面的例子:

No config file found. Please create one at ~/.kube/config.

这是一个相对更好的错误提示:

  • 具体说明了导致错误的原因;

  • 提供了解决办法

  • 体现了以用户为中心

想让你的代码得到更多人的信赖,那么以一致的方式返回错误和有用的信息是必不可少的。

9.6 避免使用 Panic

panic 是 Go 的内置函数,在发生 panic 后,会停止正常的控制流程,并终止程序运行。对于正常的错误,是不建议使用的。以下示例演示了 panic 是如何终止程序的:

package main

import (
  "fmt"
)

func main() {
  fmt.Println("This is executed")

  panic("Oh no. I can do no more. GoodBye.")

  fmt.Println("This is not executed")
}

这个程序导致了恐慌和程序崩溃。执行到第二句的时候,panic 被调用,”This is not executed”永远不会被执行。

使用 panic 的场景:

  • 程序遇到不可恢复的错误。这可能是状态丢失,或者继续执行程序将导致更多的问题。在这种情况下,最好的做法是终止程序;

  • 错误无法被处理。

9.7 延迟执行

defer 关键字用来实现延迟执行,它会在函数结束前被执行。常用于关闭打开的文件,释放资源等,也会结合 recover 函数,捕获 panic。

defer 语句后面必须是一个函数:

defer fmt.Println("I'm defer")

9.8 recover

panic 属于致命错误,会导致程序崩溃,我们可以通过 recover 函数,在 panic 出现时避免程序崩溃。

recover 函数放在 defer 调用的函数中才会有效。

一个超过索引范围的例子:

package main

import (
 "fmt"
)

func LoopArray(s1 [3]int) {

 for i := 0; i < 4; i++ {
  fmt.Println(s1[i])
 }
}

func main() {
 s := [3]int{0, 1, 2}
 LoopArray(s)
 fmt.Println("Done.")
}

代码释义:

  • 定义一个长度为3,类型为 int 的数组 s1

  • s1 的索引下标为0,1,2

  • 使用 for 语句遍历数组,遍历范围是0,1,2,3

  • 超过了索引范围,打印出 s1 的值后,程序会报 panic 错误,并结束执行

  • Done. 并不会被打印

使用 recover 捕获 panic,让程序继续:

package main

import (
 "fmt"
)

func LoopArray(s1 [3]int) {
 defer func() {
  if err := recover(); err != nil {
   fmt.Println(err)
  }
 }()
 for i := 0; i < 4; i++ {
  fmt.Println(s1[i])
 }
}

func main() {
 s := [3]int{0, 1, 2}
 LoopArray(s)
 fmt.Println("Done.")
}

我在 LoopArray 函数中,使用了 recover 函数捕获 panic,此时 panic 的错误信息会被打印,便于开发者排查错误,重要的是,”Done.”被正常打印了。

9.9 小结

本章主要介绍了 Go 语言处理错误的思想,它将错误作为函数或方法的最后一个值,返回给调用者,由调用者决定如何处理错误。介绍了错误创建的两种方式,以及如何使用 recover 让程序从 panic 中恢复。

文档更新时间: 2021-11-08 10:18   作者:admin