[Go 入门] 第九章 处理错误

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

软件不可避免地会有错误及遇到为考虑的情形, 很多语言选择在发生必须捕获的错误时引发异常, 而Go语言处理错误的方式很有趣——将错误作为一种类型, 本文内容:

错误处理及 Go 语言的独特之处

Go 语言中,一种约定是在调用可能出现问题的方法或函数时,返回一个类型为错误的值。这意味着,函数通常不会引发异常,而让调用者决定如何处理错误

1
2
3
4
5
6
7
8
func main() {
file, err := ioutil.ReadFile("foo.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(file)
}

在没有foo.txt文件的系统中运行程序时,将会触发错误

要理解Go语言处理错误的方式,重要的是明白函数ReadFile接收一个字符串参数,并返回一个字节切片和一个错误值。定义如下:

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

这意味着函数 ReadFile 总是返回一个错误值,可对其进行检查。Go语言中常见的方式为:

1
file, err := ioutil.ReadFile("foo.txt")

语法 := 是Go语言中的简短赋值语句,只能在函数中使用。Go编译器会自动推断出变量的类型,而无需显式地声明,因此这里无需告诉编译器 file 是一个字节切片,而 err 是一个错误,这样提供了极大的便利,前面的代码与下面的代码等价:
1
2
3
var file []byte
var err error
file, err = ioutil.ReadFile("foo.txt")

Go语言中约定,如果没有发生错误,返回的错误值将为 nil,这让程序调用方法或函数时,能够检查它是否像预期那样执行完毕。

1
2
3
if err != nil {
//code
}

Go语言中,这种做法很常见。有些开发人员认为,这种做法很繁琐,因为它要求调用的每个方法或函数时都检查错误,导致代码重复。

这说得也许没错,但Go语言处理错误的方式比其他语言更加灵活,因为可像其他类型一样在函数间传递错误。这意味着代码要简短得多。如想了解更多可阅读 Rob Pike 的博文 《Errors are Values》

理解错误类型

Go 语言中,错误是一个值。标准库声明了接口 error

1
2
3
type error interface {
Error() string
}

这个接口只有一个方法——Error,它返回一个字符串

创建错误

标准库中的 errors 包支持创建和操作错误

1
2
3
4
5
6
func main() {
err := errors.New("Something went wrong")
if err != nil {
fmt.Println(err)
}
}

设置错误格式

除 errors 包外,标准库中的 fmt 包还提供了方法 Errorf, 可用于设置返回的错误字符串的格式,这可以将多个值合并成更有意义的错误的字符串,从而动态地创建错误字符串

1
2
3
4
5
6
7
func main() {
name, role := "Richard Jupp", "Drummer"
err := fmt.Errorf("The %v %v quit", role, name)
if err != nil {
fmt.Println(err)
}
}

从函数返回错误

1
2
3
4
5
6
func Half(numberToHalf int) (int, error) {
if numberToHalf % 2 != 0 {
return -1, fmt.Errorf("cannot half")
}
return numberToHalf / 2, nil
}

Go语言错误处理方式的一个优点:错误处理不是在函数中,而是在调用函数的地方进行的,这在错误处理方面提供了极大的灵活性,而不是简单地一刀切

错误可用性

除从技术角度考虑 Go 语言的错误处理方式和错误生成方式外,还需从以用户为中心的角度考虑错误,用户可能会遇到错误,并尝试从错误中恢复,因此以下

You broke something ! Gook luck !

错误消息很糟糕,没有提供有关那里出现问题的线索,也没有提供有关如何恢复的建议,而下面错误消息更好:

No config file find. Please create one at ~/.foorc

原因如下:

  • 具体指出了问题
  • 提供了问题的解决方案
  • 对用户更有礼貌

如果库用户相信错误会以一致的方式返回,切包含有用的错误消息,则用户能够从错误中恢复的可能性将高很多。

慎用 Panic

panic 是 Go 语言中的一个内置函数,它终止正常的控制流程并引发恐慌(panicking),导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋的余地

1
2
3
4
5
func main() {
fmt.Println("This is executed")
panic("Oh no. I can do more. Goodbye.")
fmt.Println("This is not executed")
}

调用 panic 后,程序将停止执行,因此打印 This is not executed 的代码根本没有机会执行。

在 Go 代码中,常常滥用以下的做法,没有办法修复,只能让程序崩溃了,有些情况下,这样做是合适的,但通常不应该这样做

1
2
3
if err != nil {
panic(err)
}

在下面的情形下,使用 panic 可能是正确的选择:

  • 程序处于无法恢复的状态。这可能意味着无路可走了,或者再往下执行程序将带来更多的问题,这种情况下,最佳的选择就是让程序崩溃
  • 发生了无法处理的错误

问题列表

  • 在Go代码中,if err != nil 随处可见,这看起来重复太多,这是最佳的做法吗?

    虽然这看起来重复太多,但是能够检查错误实际上是Go语言的特色,这给程序员提供了极大的控制权。虽然可采用方法和第三方包来减少这样的重复,但大多数程序员都喜欢随心所欲的处理错误

  • Go 支持异常吗

    不同于 Java 等其他语言,Go 语言不支持传统的 try-catch-finally 控制结构吗,而向函数或方法的调用者报告错误


All articles in this blog adopt the CC BY-SA 4.0 agreement unless otherwise stated. Please indicate the source for reprinting!