Press "Enter" to skip to content

Go 错误处理篇(三):panic 和 recover

前面学院君介绍了 Go 语言通过 error 类型统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?

panic

Go 语言没有像 Java、PHP 那样引入异常的概念,也没有提供 try...catch 这样的语法对运行时异常进行捕获和处理,当代码运行时出错,而又没有在编码时显式返回错误时,Go 语言会抛出 panic,中文译作「运行时恐慌」,我们也可以将其看作 Go 语言版的异常。

除了像上篇教程演示的那样由 Go 语言底层抛出 panic,我们还可以在代码中显式抛出 panic,以便对错误和异常信息进行自定义,仍然以上篇教程除数为 0 的示例代码为例,我们可以这样显式返回 panic 中断代码执行:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("代码清理逻辑")
    }()

    var i = 1
    var j = 0
    if j == 0 {
        panic("除数不能为0!")
    }
    k := i / j
    fmt.Printf("%d / %d = %d\n", i, j, k)
}

这样一来,当我们执行这段代码时,就会抛出 panic:

-w888

panic 函数支持的参数类型是 interface{}

func panic(v interface{})

所以可以传入任意类型的参数:

panic(500)   // 传入数字
panic(errors.New("除数不能为0"))  // 传入 error 类型

无论是 Go 语言底层抛出 panic,还是我们在代码中显式抛出 panic,处理机制都是一样的:当遇到 panic 时,Go 语言会中断当前协程(即 main 函数)后续代码的执行,然后执行在中断代码之前定义的 defer 语句(按照先入后出的顺序),最后程序退出并输出 panic 错误信息,以及出现错误的堆栈跟踪信息,也就是下面红框中的内容:

-w876

第一行表示出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最后一行则是程序的退出状态,通过这些信息,可以帮助你快速定位问题并予以解决。

recover

此外,我们还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java、PHP 中 try...catch 语句的功能。

由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获应该放到 defer 语句中完成,才可以在抛出 panic 时通过 recover 函数将其捕获,defer 语句执行完毕后,会退出抛出 panic 的当前函数,回调调用它的地方继续后续代码的执行。

可以类比为 panic、recover、defer 组合起来实现了传统面向对象编程异常处理的 try…catch…finally 功能。

下面我们引入 recover() 函数来重构上述示例代码如下:

package main

import (
    "fmt"
)

func divide() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("Runtime panic caught: %v\n", err)
        }
    }()

    var i = 1
    var j = 0
    k := i / j
    fmt.Printf("%d / %d = %d\n", i, j, k)
}

func main() {
    divide()
    fmt.Println("divide 方法调用完毕,回到 main 函数")
}

如果没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息:

-w926

而现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:

-w747

如果在代码执行过程中没有抛出 panic,比如我们把 divide() 函数中的 j 值改为 1,则代码会正常执行到函数末尾,然后调用 defer 语句声明的匿名函数,此时 recover() 函数返回值为 nil,不会执行 if 分支代码,然后退出 divide() 函数回到 main() 函数执行后续代码:

-w516

这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover() 函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性。

以上就是 Go 语言错误和异常处理的全部语法,非常简单明了。接下来,我们将基于目前已经学习的基础语法对 Go 语言编程进行优化和增强 —— 介绍如何通过 Go 代码实现常见的数据结构和算法,以及如何在 Go 语言中实现常见的设计模式。

2 Comments

  1. Jason
    Jason 2021年3月11日

    学院君,最近在 laravel 学院付费全年订阅学习 Go 才知道你正在更新 2.0 版,请问在 laravel 学院订阅的在这里和 Geekr 看付费内容有效吗?

    • 学院君
      学院君 2021年3月11日

      这两个不一样 但是学院君订阅用户可以以极低的折扣购买

发表回复