《Go语言四十二章经》第十五章 错误处理

作者:李骁

15.1 错误类型

任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:

  1. err := errors.New("math - square root of negative number")
  2. func Sqrt(f float64) (float64, error) {
  3. if f < 0 {
  4. return 0, errors.New ("math - square root of negative number")
  5. }
  6. }

用 fmt 创建错误对象:

通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:

  1. if f < 0 {
  2. return 0, fmt.Errorf("square root of negative number %g", f)
  3. }

15.2 Panic

在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。

标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。

不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。

自定义包中的错误处理和 panicking,这是所有自定义包实现者应该遵守的最佳实践:

1)在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic()

2)向包的调用者返回错误值(而不是 panic)。

recover() 的调用仅当它在 defer 函数中被直接调用时才有效。

下面主函数recover 了panic:

  1. func Parse(input string) (numbers []int, err error) {
  2. defer func() {
  3. if r := recover(); r != nil {
  4. var ok bool
  5. err, ok = r.(error)
  6. if !ok {
  7. err = fmt.Errorf("pkg: %v", r)
  8. }
  9. }
  10. }()
  11. fields := strings.Fields(input)
  12. numbers = fields2numbers(fields)
  13. return
  14. }
  15. func fields2numbers(fields []string) (numbers []int) {
  16. if len(fields) == 0 {
  17. panic("no words to parse")
  18. }
  19. for idx, field := range fields {
  20. num, err := strconv.Atoi(field)
  21. if err != nil {
  22. panic(&ParseError{idx, field, err})
  23. }
  24. numbers = append(numbers, num)
  25. }
  26. return
  27. }

15.3 Recover:从 panic 中恢复

正如名字一样,这个(recover)内建函数被用于从 panic 或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。
recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。
总结:panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。

  1. func protect(g func()) {
  2. defer func() {
  3. log.Println("done")
  4. // 即使有panic,Println也正常执行。
  5. if err := recover(); err != nil {
  6. log.Printf("run time panic: %v", err)
  7. }
  8. }()
  9. log.Println("start")
  10. g() // 可能发生运行时错误的地方
  11. }

15.4 有关于defer

说到错误处理,就不得不提defer。先说说它的规则:

  • 规则一 当defer被声明时,其参数就会被实时解析
  • 规则二 defer执行顺序为先进后出
  • 规则三 defer可以读取有名返回值,也就是可以改变有名返回参数的值。

必须要先声明defer,否则不能捕获到panic异常。recover() 的调用仅当它在 defer 函数中被直接调用时才有效。

panic 是用来表示非常严重的不可恢复的错误的。在Go语言中这是一个内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。

函数执行的时候panic了,函数不往下走,开始运行defer,defer处理完再返回。这时候(defer的时候),recover内置函数可以捕获到当前的panic(如果有的话),被捕获到的panic就不会向上传递了。
recover之后,逻辑并不会恢复到panic那个点去,函数还是会在defer之后返回。

大致过程:

Panic—->defer—>recover

  1. // 规则一,当defer被声明时,其参数就会被实时解析
  2. package main
  3. import "fmt"
  4. func main() {
  5. var i int = 1
  6. defer fmt.Println("result =>", func() int { return i * 2 }())
  7. i++
  8. // 输出: result => 2 (而不是 4)
  9. }
  1. // 规则二 defer执行顺序为先进后出
  2. package main
  3. import "fmt"
  4. func main() {
  5. defer fmt.Print(" !!! ")
  6. defer fmt.Print(" world ")
  7. fmt.Print(" hello ")
  8. }
  9. //输出: hello world !!!

上面讲了两条规则,第三条规则其实也不难理解,只要记住是可以改变有名返回值:

这是由于在Go语言中,return 语句不是原子操作,最先是所有结果值在进入函数时都会初始化为其类型的零值(姑且称为ret赋值),然后执行defer命令,最后才是return操作。如果是有名返回值,返回值变量其实可视为是引用赋值,可以能被defer修改。而在匿名返回值时,给ret的值相当于拷贝赋值,defer命令时不能直接修改。

  1. func fun1() (i int)

上面函数签名中的 i 就是有名返回值,如果fun1()中定义了 defer 代码块,是可以改变返回值 i 的,函数返回语句return i 可以简写为 return 。

这里综合了一下,在下面这个例子里列举了几种情况,可以好好琢磨下;

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. fmt.Println("=========================")
  7. fmt.Println("return:", fun1())
  8. fmt.Println("=========================")
  9. fmt.Println("return:", fun2())
  10. fmt.Println("=========================")
  11. fmt.Println("return:", fun3())
  12. fmt.Println("=========================")
  13. fmt.Println("return:", fun4())
  14. }
  15. func fun1() (i int) {
  16. defer func() {
  17. i++
  18. fmt.Println("defer2:", i) // 打印结果为 defer: 2
  19. }()
  20. // 规则二 defer执行顺序为先进后出
  21. defer func() {
  22. i++
  23. fmt.Println("defer1:", i) // 打印结果为 defer: 1
  24. }()
  25. // 规则三 defer可以读取有名返回值(函数指定了返回参数名)
  26. return 0 //实际为2 。 换句话说其实怎么写都是直接 return 的效果
  27. }
  28. func fun2() int {
  29. var i int
  30. defer func() {
  31. i++
  32. fmt.Println("defer2:", i) // 打印结果为 defer: 2
  33. }()
  34. defer func() {
  35. i++
  36. fmt.Println("defer1:", i) // 打印结果为 defer: 1
  37. }()
  38. return i
  39. }
  40. func fun3() (r int) {
  41. t := 5
  42. defer func() {
  43. t = t + 5
  44. fmt.Println(t)
  45. }()
  46. return t
  47. }
  48. func fun4() int {
  49. i := 8
  50. // 规则一 当defer被声明时,其参数就会被实时解析
  51. defer func(i int) {
  52. i = 99
  53. fmt.Println(i)
  54. }(i)
  55. i = 19
  56. return i
  57. }

下面是输出,在有名返回值情况下,return语句怎么写都改变不了最终返回的实际值,在上面fun1() (i int) 中,return 100和return 0 没有任何作用,返回的还是i的实际值,所以我们一般直接写为return。这点要注意,有时函数可能返回非我们希望的值,所以改为匿名返回也是一种办法。

  1. 程序输出:
  2. =========================
  3. defer1: 1
  4. defer2: 2
  5. return: 2
  6. =========================
  7. defer1: 1
  8. defer2: 2
  9. return: 0
  10. =========================
  11. 10
  12. return: 5
  13. =========================
  14. 99
  15. return: 19

使用defer计算函数执行时间

  1. package main
  2. import(
  3. "fmt"
  4. "time"
  5. )
  6. func main(){
  7. defer timeCost(time.Now())
  8. fmt.Println("start program")
  9. time.Sleep(5*time.Second)
  10. fmt.Println("finish program")
  11. }
  12. func timeCost(start time.Time){
  13. terminal:=time.Since(start)
  14. fmt.Println(terminal)
  15. }

另外一种计算函数执行时间方法:

在对比和基准测试中,我们需要知道一个计算执行消耗的时间。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time 包中的 Now() 和 Sub 函数:

  1. start := time.Now()
  2. longCalculation()
  3. end := time.Now()
  4. delta := end.Sub(start)
  5. fmt.Printf("longCalculation took this amount of time: %s\n", delta)