Go panic的学习

一、前言

我们的应用程序常常会出现异常,包括由运行时检测到的异常或者应用开发者自己抛出的异常。

  • 异常在一些其他语言中,如c++、java,被叫做Exception,主要由抛出异常和捕获异常两部分组成。
  • 异常在go语言中,叫做panic,且由panic和recover方法组成,panic用来抛出,recover用来从panic中恢复。

1.1 panic实例分析

以下是一段简单的panic和recover使用示例:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    /*defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()*/
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    fmt.Println("Printing in g", i)
    panic(i)
    fmt.Println("After panic in g", i)
}

我们先把defer recover部分注释,运行结果如下:

Calling g.
Printing in g 0
panic: 0

goroutine 1 [running]:
main.g(0x4b14a0)
    /tmp/sandbox2444947193/prog.go:18 +0x94
main.f()
    /tmp/sandbox2444947193/prog.go:12 +0x5d
main.main()
    /tmp/sandbox2444947193/prog.go:6 +0x19

Program exited.

可以看到程序运行到g方法的第二行时,产生的panic导致进程异常退出,后续的代码都没有执行。

再把recover注释部分打开,运行结果为:

Calling g.
Printing in g 0
Recovered in f 0
Returned normally from f.

Program exited.

f方法中的recover捕获了panic,打印了panic传递的参数,并且main方法是正常返回的。g方法panic之后的代码没有执行。

1.2 官方翻译

panic是go的内置函数,它可以终止程序的正常执行流程并发出panic。

比如:当函数F调用panic,F的执行将被终止,并返回到调用者。对调用者而言,F就像调用者直接调用了panic。该过程一直跟随堆栈向上,直到当前goroutine中的所有函数都返回,此时 程序崩溃
panic可以通过直接调用panic产生。同时也可能由运行时的错误所产生,例如数组越界访问。

recover是go语言的内置函数,它的唯一作用是可以从panic中重新控制goroutine的执行。recover必须通过defer来运行

在正常的执行流程中,调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panic,recover将会捕获到panic抛出的信息,同时恢复其正常的执行流程。

小结

  1. panic可以令程序崩溃(异常退出)
  2. recover可以让程序从panic中恢复,并正常运行
  3. 即使单个goroutine中发生了panic,也会使整个进程崩溃
  4. recover必须通过defer来运行

二、实现原理

2.1 panic从哪来

我们可以手动调用内置函数panic,但是那些空指针、数组越界等运行时panic是如何被检测到的,下面针对这一问题做一些代码调试

2.1.1 常见的几种panic

  • 空指针 invalid memory address or nil pointer dereference
  • 数组越界 index out of range;slice bounds out of range
  • 除数为零 integer divide by zero
  • 自定义panic

2.1.2 追踪panic来源

测试代码

package main
func main() {
    a := 0
    testDivide(a) //除零
    //testOutRange() //越界
    //testNil() //空指针
    //panic("666") //自定义panic
}
func testDivide(a int) {
    b := 10 / a
    _ = b
}
func testOutRange() {
    var a []int
    a[0] = 2
}
func testNil() {
    var a *int
    *a = 1
}

调试代码

与linux平台下的gdb调试工具类似,dlv用来调试go语言编写的程序。

dlv是一个命令行工具,它包含了多个调试命令,例如运行程序、下断点、打印变量、step in、step out等。我们常用的go语言编辑器,如vscode、golang等的可视化调试也是调用dlv。

找出panic是怎么产生的:

这里我们先给出结论,具体调试过程产生的代码,请往下看

调试自定义panic方法:

  1. 在8行处下断点
  2. 打印main方法的汇编代码
  3. 可以看到panic方法编译后实质是runtime包中的gopanic方法

使用dlv调试testDivide中的代码,有以下几个关键步骤:

  1. 在12行处下断点
  2. 打印testDivide方法的汇编代码
  3. testDivide方法中测试参数a的值是否为零
  4. 如果为零,则调用runtime包的panicdivide方法
  5. 调用runtime包的panicdivide方法
  6. panicdivide方法调用了panic
  7. 打印panicdivide的汇编代码,panic方法编译后实质是runtime包中的gopanic方法

所以其实panic方法实际调用了runtime.gopanic

  • 编译后的testDivide方法中除了正常的除法逻辑,编译器塞入了判断除数是否为零的代码分支,当除数为零则进入panic流程,与自定义panic相同,同样调用了runtime.gopanic
  • 其他数组越界及空指针,也都是调用了runtime.gopanic进入panic流程,不同的是:数组越界与除数为零相似,是通过编译器塞入判断分支进行越界检测;而空指针是通过访问非法地址产生中断进入panic流程。

小结

  • panic可以由开发者调用内置函数抛出
  • 编译器将检测异常的代码加入到程序中,会出现异常时抛出
  • 某些非法指令产生中断,并由中断处理函数抛出

2.2 panic到哪去

2.2.1 panic后的处理流程

由于panic和defer有着难解难分的关系,我们先了解一下defer。

defer定义的官翻:

defer语句将函数调用保存到一个列表上。保存的调用列表在当前函数返回前执行。Defer通常用于简化执行各种清理操作的函数。

通俗地说,就是defer保证函数调用不管在什么情况下(即使当前函数发生panic),在当前函数返回前必然执行。另外defer的函数调用符合先进后出的规则,即先defer的函数后执行。

我们看一个示例程序,它是第一节示例程序的升级版本,方法g中会调用自身:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("defer in main")
    }()
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    /*defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()*/
    defer func() {
        fmt.Println("defer in f")
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

程序运行结果如下:

作者:刘玮
链接:https://www.zhihu.com/question/295517993/answer/2421882834
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
defer in f
defer in main
panic: 4

goroutine 1 [running]:
main.g(0x4)
        /tmp/sandbox2114608904/prog.go:30 +0x1ec
main.g(0x3)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x2)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x1)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x0)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.f()
        /tmp/sandbox2114608904/prog.go:23 +0x7f
main.main()
        /tmp/sandbox2114608904/prog.go:9 +0x3f

Program exited

从运行结果可以观察到defer的作用,即使方法g中当i为4时发生了panic,每个defer的函数调用依然正常被执行了,而且是先进后出的顺序被执行。就像是每次defer时,将被defer的函数调用push到一个栈数据结构中,当返回时,再从栈中挨个将defer的函数pop出来并执行。

recover函数调用必须使用defer关键字,就是因为defer的函数调用必然会被执行。可以将以上实例中defer recover部分打开观察输出,与第一节中defer recover输出类似,程序可以正常执行并正常退出。

2.2.2 源码分析

我们再对源码做一下简单分析,以加深对panic及recover处理流程的理解。

首先简单了解下有关defer的一对方法:deferproc和deferreturn。

  • deferproc即defer关键字的实现,它将defer的函数调用push到当前goroutine中的defer链表头部
  • deferreturn,当一个函数中包含defer操作,编译器将在函数返回前插入一条deferreturn调用,deferreturn会将当前函数中defer的函数调用依次执行完毕

panic方法对应的实现为runtime.gopanic,recover方法对应的实现为runtime.gorecover。

源码如下(为了简化理解,省略了很多分支判断,只保留主流程的代码):

func gopanic(e interface{}) {
    //获取当前goroutine的对象gp
    gp := getg()
    ...
    //将当前panic添加到gp的panic链表头部
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    ...
    //循环执行defer链表中的函数
    for {
        //获取gp的defer链表
        d := gp._defer
        if d == nil {
            //如果没有defer,退出循环
            break
        }
        ...
        done := true
        ...
        //执行defer的函数调用
        var regs abi.RegArgs
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz), uint32(d.siz), &regs)
        ...
        p.argp = nil
        d._panic = nil
        ...
        if done {
            //清理defer对象,并设置下一个defer对象到gp的defer链表头部
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
        }
        if p.recovered {
            //如果defer运行了recover函数,调用内置的recovery函数恢复调用
            //recovery函数会将当前的调用栈改变到deferreturn,从而使得程序可以继续正常运行
            ...
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    //如果没有recover,defer执行完毕,打印panic信息,并退出进程
    preprintpanics(gp._panic)
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

//recover方法的实现
func gorecover(argp uintptr) interface{} {

    gp := getg()
    p := gp._panic
    ...
    //recover方法仅有的一个作用,将recovered置为true
    p.recovered = true
    return p.arg
}

小结

  • panic处理过程中会检测是否有defer的函数调用
  • 如果有,按照先进后出的顺序依次执行
  • 如果defer中有recover调用,则将调用栈修改到deferreturn,使得程序正常执行
  • 否则当defer的函数调用执行完后,打印panic信息,进程退出

2.3 panic 打印信息

最后我们通过一个简单的例子,看一下recover后如何打印panic信息,及如何阅读panic信息

示例是一个除零的panic:

  1. recover后,调用printPanicInfo方法
  2. printPanicInfo使用runtime.Stack方法收集调用堆栈信息
  3. r为recover返回的参数,即panic传入的参数,一般为panic的具体原因,本示例为:runtime error: integer divide by zero
  4. 将panic原因和堆栈信息拼接并打印
package main
import (
    "fmt"
    "runtime"
)
func main() {
    f()
}
func f() {
    defer func() {
        if r := recover(); r != nil {
            printPanicInfo(r)
        }
    }()
    g()
}
func g() {
    a := 10
    var b int
    a = a / b
}
func printPanicInfo(r interface{}) {
    buf := make([]byte, 64<<10)
    buf = buf[:runtime.Stack(buf, false)]
    s := fmt.Sprintf("%s\n%s", r, buf)
    fmt.Println(s)
}

输出为:

作者:刘玮
链接:https://www.zhihu.com/question/295517993/answer/2421882834
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

//panic的原因
runtime error: integer divide by zero  
//goroutine的id
goroutine 1 [running]: 
//下面是runtime.Stack方法调用时的调用堆栈链,方法名称和方法被调用的文件行数成对出现
main.printPanicInfo(0x4b78c0, 0x572a10) //方法名称
        E:/xxx/liuwei/test/main.go:29 +0x74 //方法所在的文件和行数
main.f.func1()
        E:/xxx/liuwei/test/main.go:15 +0x59
panic(0x4b78c0, 0x572a10)
        C:/go1.13/go/src/runtime/panic.go:679 +0x1c0 //panic被调用
main.g(...)
        E:/xxx/liuwei/test/main.go:24 //发生panic的代码行数
main.f()
        E:/xxx/liuwei/test/main.go:18 +0x50
main.main()
        E:/xxx/liuwei/test/main.go:9 +0x27

打印的信息中主要由panic原因调用堆栈组成,我们阅读堆栈信息时,可以首先找到runtime.panic,它的下一条堆栈记录就是发生panic的代码具体行数。然后再结合panic的原因信息,一般会很快了解到panic发生的原因。

另外除了panic之外还有一种fatalpanic,这种严重的异常无法使用recover恢复,一般是运行时检测到不可恢复的操作时抛出。例如发生map并发写时会throw(“concurrent map writes”),导致进程崩溃。

特别提示

  1. 因为Golang的gorotuine机制,panic在不同的gorotuine里面,是单独的,并不是整体处理。可能一个地方挂了,就会整体挂掉,这个要非常小心。

三、总结

  1. panic() 会退出进程,是因为调用了 exit 的系统调用;
  2. recover() 并不是说只能在 defer 里面调用,而是只能在 defer 函数中才能生效,只有在 defer 函数里面,才有可能遇到 _panic 结构;
  3. recover() 所在的 defer 函数必须和 panic 都是挂在同一个 goroutine 上,不能跨协程,因为 gopanic 只会执行当前 goroutine 的延迟函数;
  4. panic 的恢复,就是重置 pc 寄存器,直接跳转程序执行的指令,跳转到原本 defer 函数执行完该跳转的位置(deferreturn 执行),从 gopanic 函数中跳出,不再回来,自然就不会再 fatalpanic;
  5. panic 为啥能嵌套?这个问题就像是在问为什么函数调用可以嵌套一样,因为这个本质是一样的。

参考资料
6. 深度细节 | Go 的 panic 秘密都在这
7. go panic 的实现原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/966.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

进阶C语言——指针(二)【题目练习】

文章目录1.指针和数组概念的理解2.指针和数组笔试题解析一维数组字符数组二维数组1.指针和数组概念的理解 指针和数组 数组&#xff1a;能够存放一组相同类型的元素&#xff0c;数组的大小取决于数组的元素个数和元素类型指针&#xff1a;也是地址或指针变量&#xff0c;大小是…

Spring Cloud -- GateWay

为什么需要网关在微服务架构中&#xff0c;一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢&#xff1f;如果没有网关的存在&#xff0c;我们只能在客户端记录每个微服务的地址&#xff0c;然后分别去调用。这样的话会产生很多问题&#xff0c;例…

重构·改善既有代码的设计.04之重构手法(下)完结

1. 前言 本文是代码重构系列的最后一篇啦。前面三篇《重构改善既有代码的设计.01之入门基础》、《重构改善既有代码的设计.02之代码的“坏味道”》、《重构改善既有代码的设计.03之重构手法&#xff08;上&#xff09;》介绍了基础入门&#xff0c;代码异味&#xff0c;还有部…

【Java】你真的懂封装吗?一文读懂封装-----建议收藏

博主简介&#xff1a;努力学习的预备程序媛一枚~博主主页&#xff1a; 是瑶瑶子啦所属专栏: Java岛冒险记【从小白到大佬之路】 前言 write in the front: 如何理解封装&#xff1f; 试想&#xff1a;我们使用微波炉的时候&#xff0c;只用设置好时间&#xff0c;按下“开始”…

[C++]反向迭代器

目录 前言&#xff1a; 1 对反向迭代器的构造思想 2 实现反向迭代器 3 完整代码 前言&#xff1a; 本篇文章主要介绍了STL容器当中的反向迭代器&#xff0c;可能有朋友会说&#xff1a;“反向迭代器有什么好学的&#xff1f;不一样还是迭代器吗&#xff0c;我正向能写出来&…

【js逆向】hook大全

▒ 目录 ▒&#x1f6eb; 导读需求1️⃣ 普通函数2️⃣ 对象方法&#xff08;Class.prototype&#xff09;3️⃣ 对象属性&#xff08;Object.defineProperty&#xff09;4️⃣ Proxy5️⃣ 批量hook示例&#x1f6ec; 文章小结&#x1f4d6; 参考资料&#x1f6eb; 导读 需求 …

【面试题系列】K8S常见面试题

目录 序言 问题 1. 简单说一下k8s集群内外网络如何互通的吧 2.描述一下pod的创建过程 3. 描述一下k8s pod的终止过程 4.Kubernetes 中的自动伸缩有哪些方式&#xff1f; 5.Kubernetes 中的故障检测有哪些方式&#xff1f; 6.Kubernetes 中的资源调度有哪些方式&#xff…

如何优雅的用POI导入Excel文件

在企业级项目开发中&#xff0c;要经常涉及excel文件和程序之间导入导出的业务要求&#xff0c;那么今天来讲一讲excel文件导入的实现。java实现对excel的操作有很多种方式&#xff0c;例如EasyExcel等&#xff0c;今天我们使用的是POI技术实现excel文件的导入。POI技术简介1.P…

全连接神经网络

目录 1.全连接神经网络简介 2.MLP分类模型 2.1 数据准备与探索 2.2 搭建网络并可视化 2.3 使用未预处理的数据训练模型 2.4 使用预处理后的数据进行模型训练 3. MLP回归模型 3.1 数据准备 3.2 搭建回归预测网络 1.全连接神经网络简介 全连接神经网络(Multi-Layer Percep…

基于Vue3和element-plus实现一个完整的登录功能

先看一下最终要实现的效果:登录页面:注册页面:(1)引入element-plus组件库引入组件库的方式有好多种,在这里我就在main.js全局引入了.npm i element-plus -Smain.js中代码:import { createApp } from "vue"; //element-plus import ElementPlus from "element-pl…

双指针 -876. 链表的中间结点-leetcode

开始一个专栏&#xff0c;写自己的博客 双指针&#xff0c;也算是作为自己的笔记吧&#xff01; 双指针从广义上来说&#xff0c;是指用两个变量在线性结构上遍历而解决的问题。狭义上说&#xff0c; 对于数组&#xff0c;指两个变量在数组上相向移动解决的问题&#xff1b;对…

「SAP ABAP」OPEN SQL(四)【FROM语句】

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计学专业大二本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后…

女子举重问题

一、问题的描述 问题及要求 1、搜集各个级别世界女子举重比赛的实际数据。分别建立女子举重比赛总成绩的线性模型、幂函数模型、幂函数改进模型&#xff0c;并最终建立总冠军评选模型。 应用以上模型对最近举行的一届奥运会女子举重比赛总成绩进行排名&#xff0c;并对模型及…

【2023-03-10】JS逆向之美团滑块

提示&#xff1a;文章仅供参考&#xff0c;禁止用于非法途径 前言 目标网站:aHR0cHM6Ly9wYXNzcG9ydC5tZWl0dWFuLmNvbS9hY2NvdW50L3VuaXRpdmVsb2dpbg 页面分析 接口流程 1.https://passport.meituan.com/account/unitivelogin主页接口&#xff1a;需获取下面的参数&#xff0…

力扣刷题---初始链表1

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f; c语言初阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:讲解初始数据结构链表的三个力扣题 1.移除链表元素. 2.反转…

Visual Studio Code 1.76 发布

欢迎使用 Visual Studio Code 2023 年 2 月版&#xff0c;其中一些亮点包括&#xff1a; 配置文件 - 活动配置文件徽章&#xff0c;通过命令面板快速切换配置文件。辅助功能改进 - 新的音频提示&#xff0c;改进的终端屏幕阅读器模式。可移动的 Explorer 视图- 将资源管理器放…

JavaWeb——Request(请求)和Response(响应)介绍

在写servlet时需要实现5个方法&#xff0c;在一个service方法里面有两个参数request和response。 浏览器向服务器发送请求会发送HTTP的请求数据——字符串&#xff0c;这些字符串会被Tomcat所解析&#xff0c;然后这些请求数据会被放到一个对象(request)里面保存。 相应的Tom…

有图解有案例,我终于把 Condition 的原理讲透彻了

哈喽大家好&#xff0c;我是阿Q&#xff01; 20张图图解ReentrantLock加锁解锁原理文章一发&#xff0c;便引发了大家激烈的讨论&#xff0c;更有小伙伴前来弹窗&#xff1a;平时加解锁都是直接使用Synchronized关键字来实现的&#xff0c;简单好用&#xff0c;为啥还要引用Re…

React面向组件编程(理解与使用+state+props+refs与事件处理)

1 基本理解与使用 函数式组件 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"…

开发板与ubantu文件传送

接下来的所以实验都通过下面这种方式发送APP文件到开发板运行 目录 1、在ubantu配置 ①在虚拟机上添加一个桥接模式的虚拟网卡 ②设定网卡 ③在网卡上配置静态地址 2、开发板设置 ①查看网卡 ②配置网卡静态ip 3、 测试 ①ping ②文件传送 传送报错情况 配置环境&#…
最新文章