程序错误在程序运行中经常发生,程序的错误又可以分为两种:一种是逻辑错误,我们一般都是可以预料到的错误,如Go中的error,出现这类错误如果不处理造成的后果一般是业务逻辑错误,处理方式一般是靠判断函数返回值来处理;另一种是堆栈错误,如Go中的panic,这种一般都不太能预先知晓,比如内存越界等错误,如果不处理会引发程序crash,这种错误大家更倾向称之为程序异常,处理方式是使用编程语言的异常捕获机制来处理。

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

C中没有异常捕捉机制,如果发生数组越界程序肯定崩溃;C++中提供了try…catch…finally机制来捕获异常;而Go是提供panic来跑出异常,使用defer..recover来捕获异常。

Go的异常处理机制依赖三个内置函数:panic, defer, recover。下面就重点介绍这三个内置函数是如何配合完成一次异常处理的:

  1. panic抛出异常
  2. defer保证函数退出时最后执行的动作
  3. recover捕捉panic,提供控制权

panic

panic作为Go的内置函数,如果该函数被调用后,就会终止其后面要执行的代码。如果这个painc没有被捕获,那整个程序就会退出,并打印出调用栈报告错误,我们可以根据打印出来的调用栈信息定位到发生panic的具体位置。

panic中文翻译为恐慌,个人认为这个词用得很传神,当panic发生时,表示程序发生异常了,如果我们不捕获这个异常并执行相应的处理,那么恐慌就会迅速扩散直到产生严重后果—coredump。如果我们捕获了这个panic,就是意味着这个异常仍处于我们的意料之中,我们适当处理即可,不会产生严重影响。

引发panic有哪些情况呢?我总结了以下几种:

  1. 数组越界
  2. 使用空指针
  3. 误用互斥锁
  4. 对已关闭的channel发送数据
  5. 除0
  6. 主动调用了内置函数panic

如何自定义抛出一个异常?可以参考下面这个例子:

  1. package main
  2. // main.go
  3. import (
  4. "fmt"
  5. )
  6. func errFun() {
  7. fmt.Println("call errFunc start")
  8. panic("errFun panic")
  9. fmt.Println("call errFunc end")
  10. }
  11. func main() {
  12. errFun()
  13. fmt.Println("Program finished succ!")
  14. }

输出如下,报错打印可以看出具体发送panic的文件和代码位置,方便开发者查找问题。

call errFunc start
panic: errFun panic

goroutine 1 [running]:
main.errFun()
        /mnt/e/services/gogo/main.go:11 +0x95
main.main()
        /mnt/e/services/gogo/main.go:16 +0x22
exit status 2

有些panic我们并不希望捕捉它,因为有些程序的异常可能以程序退出的方式来处理反而是最优解;但并不是所有panic都可以放任不管,比如有些http请求引发的panic,不应该导致我们http server的宕机,优雅的处理方式是提前结束这个请求并报警给开发者即可。使用defer+recover的组合可以优雅捕捉到panic。

defer

当你需要在函数执行完成后再执行一段逻辑,那defer可以很好地完成这个功能,即使是panic发生时,程序无法往下执行时也会保证能走到defer函数的处理,待defer处理完后,再结束整个函数逻辑。这个特性非常好用,比如我们可以函数退出时加一些关闭操作(channel,fd等),也可以处理panic。

比如下面这个程序,我在main中加了defer操作,在main结束前会执行我们定义的defer逻辑。

package main

// main.go

import (
    "fmt"
)

func errFun() {
    fmt.Println("call errFunc start")
    panic("errFun panic")
    fmt.Println("call errFunc end")
}

func main() {
    defer func() {
        fmt.Println("main defer call")
    } ()
    errFun()
    fmt.Println("Program finished succ!")
}

输出如下,可以看到"Program finished succ!"这句没有打印,表示panic发生代码之后的逻辑都不会继续执行了,但是main defer call打印了,说明不管如何panic,defer逻辑都会在函数退出时执行到的。

call errFunc start
main defer call
panic: errFun panic

goroutine 1 [running]:
main.errFun()
        /mnt/e/services/gogo/main.go:11 +0x95
main.main()
        /mnt/e/services/gogo/main.go:19 +0x48
exit status 2

recover

recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

观察下面这个例子,这里写了两个有问题的函数,一个是divideZero,模拟除0错误,一个是getArray,模拟数组越界,都会触发panic。第一个例子触发的是数组越界错误,我们使用defer+recover的机制捕捉它。

package main

// main.go

import (
    "fmt"
)

var arr [3]int

func divideZero(a,b int) int {
    fmt.Println("divideZero fun in")
    return a / b
}

func getArray(index, v int) {
    fmt.Println("getArray fun in")
    val := arr[index]
    divideZero(val, v)
}


func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("main defer call")
        }
    } ()

    for i:=0; i<3; i++ {
        arr[i] = i+1
    } 

    //fmt.Println("call getArray1")
    //getArray(2,1) // 正常
    //fmt.Println("call getArray2")
    getArray(3,1)  //引发数组越界错误
    fmt.Println("call getArray3")
    //getArray(2,0) //引发除0错误
    fmt.Println("Program finished succ!")
}

输出可以看出,getArray执行了,因为数组越界导致divideZero没有执行,抛出panic后被main的recover捕获了。

getArray fun in
main defer call

第二个例子,同样是调用getArray,但是没有发生数组越界,然后继续执行divideZero,发生处理异常,抛出panic,getArray没有捕获panic,因此panic继续往上抛,知道被main的recover捕获处理。

package main

// main.go

import (
    "fmt"
)

var arr [3]int

func divideZero(a,b int) int {
    fmt.Println("divideZero fun in")
    return a / b
}

func getArray(index, v int) {
    fmt.Println("getArray fun in")
    val := arr[index]
    divideZero(val, v)
}


func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("main defer call")
        }
    } ()

    for i:=0; i<3; i++ {
        arr[i] = i+1
    } 

    //fmt.Println("call getArray1")
    //getArray(2,1) // 正常
    //fmt.Println("call getArray2")
    //getArray(3,1)  //引发数组越界错误
    fmt.Println("call getArray3")
    getArray(2,0) //引发除0错误
    fmt.Println("Program finished succ!")
}

输出

call getArray3
getArray fun in
divideZero fun in
main defer call

下面这个例子比较好得模拟了一个事件主循环,根据不同的事件v的不同执行不同的逻辑,即使是某个分支发生了panic,也不会导致程序退出。在mainLoop()和divideZero()设置了recover,当divideZero抛出panic时,divideZero自己就能做好异常处理,不会影响mainLoop逻辑,而getArray函数没有设置recover,panic时就会往上抛,直到被mainLoop的recover捕获。因为mainLoop中捕获了panic,然后mainLoop退出,在main主循环里再次启动mainLoop(),事件主循环逻辑继续执行。

package main

// main.go

import (
    "fmt"
    "time"
)

var arr [3]int

func divideZero(a,b int) int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("divideZero defer call !!!! ")
        }
    } ()

    fmt.Println("divideZero fun in ", a, b)
    return a / b
}

func getArray(index, v int) {
    fmt.Println("getArray fun in ", index, v)
    val := arr[index]
    fmt.Println("getArray fun in2 ", index, val)
}

func mainLoop() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("mainLoop defer call !!!! ")
        }
    } ()

    for {
        time.Sleep(time.Second * 1)
        v := time.Now().Unix() % 10
        if v % 3 == 0 {
            getArray(3,1)  //引发数组越界错误
        } else if v % 3 == 1{
            divideZero(2,0) //引发除0错误
        } else {
            getArray(2,1) // 正常
        }
    }
}


func main() {
    for i:=0; i<3; i++ {
        arr[i] = i+1
    } 

    for {
        fmt.Println("main call mainLoop !!!! ")
        mainLoop()
    }
}

输出如下,不管逻辑函数怎么引发panic,都一定会被mainLoop函数中的recover捕捉到,因此程序一直可以循环执行。

main call mainLoop !!!! 
divideZero fun in  2 0
divideZero defer call !!!! 
getArray fun in  2 1
getArray fun in2  2 3
getArray fun in  3 1
mainLoop defer call !!!! 
main call mainLoop !!!! 
getArray fun in  3 1
mainLoop defer call !!!! 
main call mainLoop !!!! 
divideZero fun in  2 0
divideZero defer call !!!! 
getArray fun in  2 1
getArray fun in2  2 3
getArray fun in  3 1
mainLoop defer call !!!! 
main call mainLoop !!!!

如果我们觉得函数中某段代码有点虚,想捕捉这段代码的panic,应该怎么写呢?可以使用匿名函数完成这个需求,如下所示,匿名函数捕捉了val := arr[index]引发的panic,恢复处理后val := arr[index]后面的逻辑也能顺利继续进行下去了。

func getArray(index, v int) {
    fmt.Println("getArray fun in ", index, v)

    func() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("getArray defer call !!!! ")
            }
        } ()

        val := arr[index]
        fmt.Println("getArray fun in2 ", index, val)
    } ()
    fmt.Println("getArray fun in3 ", index, v)
}