【Go进阶—基础特性】panic 和 recover

尾调露台
• 阅读 2102

panic 和 recover 也是常用的关键字,这两个关键字与上一篇提到的 defer 联系很紧密。用一句话总结就是:调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;而 recover 可以中止 panic 造成的程序崩溃,不过它只能在 defer 中发挥作用。

panic

panic 是一个内置函数,接受一个任意类型的参数,参数将在程序崩溃时打印出来,如果被 recover 恢复的话,该参数也是 recover 的返回值。panic 可以由程序员显式触发,运行时遇到意料之外的错误如内存越界时也会触发。

在上一篇中我们知道每个 Goroutine 都维护了一个 _defer 链表(非开放编码情况下),执行过程中每遇到一个 defer 关键字都会创建一个 _defer 实例插入链表,函数退出时一次取出这些 _defer 实例并执行。panic 发生时,实际上是触发了函数退出,也即把执行流程转向了 _defer 链表。

panic 的执行过程中有几点需要明确:

  • panic 会递归执行当前 Goroutine 中所有的 defer,处理完成后退出;
  • panic 不会处理其他 Goroutine 中的 defer;
  • panic 允许在 defer 中多次调用,程序会终止当前 defer 的执行,继续之前的流程。

数据结构

panic 关键字在 Go 语言中是由数据结构 runtime._panic 表示的。每当我们调用 panic 都会创建一个如下所示的数据结构:

type _panic struct {
    argp      unsafe.Pointer
    arg       interface{}
    link      *_panic
    recovered bool
    aborted   bool
    goexit    bool
}
  • argp 是指向 defer 函数参数的指针;
  • arg 是调用 panic 时传入的参数;
  • link 指向前一个_panic 结构;
  • recovered 表示当前 _panic 是否被 recover 恢复;
  • aborted 表示当前的 _panic 是否被终止;
  • goexit 表示当前 _panic 是否是由 runtime.Goexit 产生的。

_panic 链表与 _defer 链表一样,都是保存在 Goroutine 的数据结构中:

type g struct {
    // ...
    _panic    *_panic
    _defer    *_defer
    // ...
}

执行过程

编译器会将关键字 panic 转换成 runtime.gopanic 函数,我们来看一下它的核心代码:

func gopanic(e interface{}) {
    gp := getg()
    ...
    var p _panic       // 创建新的 _panic 结构
    p.arg = e          // 存储 panic 的参数
    p.link = gp._panic 
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 这两行是将新结构插入到当前 Goroutine 的 panic 链表头部

    for {
        d := gp._defer // 开始遍历 _defer 链表
        if d == nil {
            break
        }

        // 嵌套 panic 的情形
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true // 标记之前 _defer 中的 _panic 为已终止
            }
            // 从链表中删除本 defer
            d._panic = nil
            if !d.openDefer {
                d.fn = nil
                gp._defer = d.link
                freedefer(d)
                continue
            }
        }

        d.started = true // 标记 defer 已经开始执行

        d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 标记触发 defer 的 _panic

        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) // 执行 defer 函数,省略对开放编码 _defer 的额外处理

        d._panic = nil
        d.fn = nil
        gp._defer = d.link

        pc := d.pc
        sp := unsafe.Pointer(d.sp)
        freedefer(d)
        // 如果被 recover 恢复的话,处理下面的逻辑
        if p.recovered {
            // ...
            gp._panic = p.link
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil {
                gp.sig = 0
            }

            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    fatalpanic(gp._panic) // 终止整个程序
    *(*int)(nil) = 0
}

该函数的执行过程包含以下几个步骤:

  1. 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;
  2. 判断是否是嵌套 panic 的情形,进行相关标记和处理;
  3. 不断从当前 Goroutine 的 _defer 链表中获取 _defer 并调用 runtime.reflectcall 运行延迟调用函数;
  4. 调用 runtime.fatalpanic 中止整个程序。

recover

recover 也是一个内置函数,用于消除 panic 并使程序恢复正常。recover 的执行过程也有几点需要明确:

  • recover 的返回值就是消除的 panic 的参数;
  • recover 必须直接位于 defer 函数内(不能出现在另一个嵌套函数中)才能生效;
  • recover 成功处理异常后,函数不会继续处理 panic 之后的逻辑,会直接返回,对于匿名返回值将返回相应的零值。

执行过程

编译器会将关键字 recover 转换成 runtime.gorecover:

func gorecover(argp uintptr) interface{} {
    gp := getg()
    p := gp._panic
    if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

函数的实现很简单,获取当前 Goroutine 中的 _panic 实例,在符合条件的情况下将 _panic 实例的 recovered 状态标记为 true,然后返回 panic 函数的参数。

我们来看一下 recover 的几个生效条件:

  • p != nil:必须存在 panic;
  • !p.goexit:非 runtime.Goexit();
  • !p.recovered:还未被恢复;
  • argp == uintptr(p.argp):recover 必须在 defer 中直接调用。

首先,必须存在 panic,runtime.Goexit() 产生的 panic 无法被恢复,这些没什么好说的。假设函数包含多个 defer,前面的 defer 通过 recover 消除 panic 后,剩余 defer 中的 recover 不能再次恢复。

有一点会让人感到疑惑,recover 函数没有参数,为什么 gorecover 函数却有参数?这正是为了限制 recover 必须在 defer 中被直接调用。gorecover 函数的参数为调用 recover 函数的参数地址,_panic 结构中保存了当前 defer 函数的参数地址,如果二者一致,说明 recover 是在 defer 中被直接调用。示例如下:

func test() {
    defer func() { // func A
        func() { // func B
            // gorecover 的参数 argp 为 B 的参数地址,p.argp 为 A 的参数的指针
            // argp != p.argp,无法恢复
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()
    }()
}
点赞
收藏
评论区
推荐文章
Irene181 Irene181
4年前
一篇文章带你了解Python递归函数
一、什么是递归函数?在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。二、函数的递归调用原理实际上递归函数是在栈内存上递归执行的,每次递归执行一次就会耗费一些栈内存。栈内存的大小是限制递归深度的重要因素三、案例分析1.求阶乘计算阶乘n!1x2x3x…xn,可以用
peter peter
4年前
深入剖析 defer 原理篇 —— 函数调用的原理?
本篇文章是深入剖析golang的defer的基础知识准备,如果要完全理解defer,避免踩坑,这个章节的基础知识必不可少。我们先复习一个最基础的知识——函数调用。这个对理解defer在函数里的行为必不可少。那么,当你看到一个函数调用的语句你能回忆起多少知识点呢?地址空间下图是一个典型的操作系统的地址空间示意图:(h
Stella981 Stella981
3年前
Golang中defer、return、返回值之间执行顺序的坑
原文链接:https://studygolang.com/articles/4809Go语言中延迟函数defer充当着cry...catch的重任,使用起来也非常简便,然而在实际应用中,很多gopher并没有真正搞明白defer、return和返回值之间的执行顺序,从而掉进坑中,今天我们就来揭开它的神秘面纱!先来运行下面两段代码:A.无名返
Stella981 Stella981
3年前
Golang WaitGroup源码分析
针对Golang1.9的sync.WaitGroup进行分析,与Golang1.10基本一样除了将panic改为了throw之外其他的都一样。源代码位置:sync\waitgroup.go。结构体typeWaitGroupstruct{noCopynoCopy//noCopy可以嵌入到结构中
Stella981 Stella981
3年前
JavaScript之函数
    玩js自然要和函数打交到。函数嘛简单来说就是给代码分个块,方便调用、信息隐藏和代码复用,还可以用于指定对象的行为。另外函数还可以玩出很多花样来。。。JavaScript使用关键字function定义函数。定义一个函数://函数声明//这种定义函数的好处是可以在当前作用域内任何位置调用,因为变量的声明和函数的
Stella981 Stella981
3年前
Golang Package
        Golang'slog模块主要提供了3类接口。分别是“Print、Panic、Fatal”,对每一类接口其提供了3中调用方式,分别是"Xxxx、Xxxxln、Xxxxf",基本和fmt中的相关函数类似,下面是一个Print的示例:packagemain
Wesley13 Wesley13
3年前
go的异常处理
最近有项目要用到cgo,对接外援写的so的库,期间cgo的代码总是不经意的崩溃啊,异常退出,种种鬼怪。cgopanic错误//刚开始对接的时候,调用cgo的程序会抛出panic,但是并不退出主程序。处理方法:在goroutine内defer处理此panicrecover后进行相应的错误处理
Wesley13 Wesley13
3年前
初探 Objective
作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和
Wesley13 Wesley13
3年前
Illegal instruction
1 现象Kernelpanicnotsyncing:Attemptedtokillinit!\<c0069278\(unwind\_backtrace0x0/0xe4)from\<c0315360\(panic0x68/0x190)\<c0315360\(panic0x68/0
铁扇公主 铁扇公主
1年前
代码编辑器 Nova for mac中文
Nova是一款强大的文本代码编辑器,由Panic开发,可以替代Coda。它具有强大的API和内置的扩展浏览器,非常易于扩展,并已内置支持多种编程语言,如CoffeeScript、CSS、Diff、ERB、Haml、HTML、INI、JavaScript等。
燕青 燕青
1年前
Macos强大的ftp客户端:Transmit 5 for Mac中文版
Transmit5是一款由Panic开发的功能强大的FTP(文件传输协议)客户端软件,专为macOS平台设计。它提供了简单、直观的界面和丰富的功能,使用户能够轻松地管理和传输文件。在文件传输和同步方面,Transmit5提供了强大的文件同步功能,可以帮助用