【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案

本文主要是介绍【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案

题目有点夸张,标题党一把,哈哈,不过也确实是在一个小的面试中碰到这个题目,然后当时经过我反复斟酌之后,愉快的写下了一个错误的答案,回来之后,自己验证了一下,于是就有了这篇文章,大神请绕道。
废话不多说直接上题目,说有如下程序(main.go),写出运行之后的结果:

package mainimport "fmt"func main(){defer_call()fmt.Println("333 Helloworld")
}func defer_call()  {defer func(){fmt.Println("11111")}()defer func(){fmt.Println("22222")}()defer func() {if r := recover(); r!= nil {fmt.Println("Recover from r : ",r)}}()defer func(){fmt.Println("33333")}()fmt.Println("111 Helloworld")panic("Panic 1!")panic("Panic 2!")fmt.Println("222 Helloworld")
}

我直接贴出运行结果:

111 Helloworld
33333
Recover from r :  Panic 1!
22222
11111
333 Helloworld

如果你做对了,建议跳过。其实我也只是把自己的验证过程记录如下,以便以后查阅。

我们用上一篇文章所搭建的golang的gdb调试环境来具体分析下为什么会是这个结果。

编译源代码使用以下命令, 这里的-l参数的意思和上面一样, 如果有需要还可以加-N参数:

/home/james/workspace/go_src/bin/go build -gcflags "-l" main.go

对这个编译方法有疑问的可以参考上一篇文章。
编译后使用gdb运行:
这里写图片描述
go里面的函数符号名称的命名规则是包名称.函数名称, 例如主函数的符号名称是main.main, 运行时中的newobject的符号名称是runtime.newobject.
首先给主函数下一个断点,给我们第一个panic("Panic 1!")所在行下一个断点,然后运行:
这里写图片描述
单步运行之后,我们可以找到panic函数所对应的源码:
这里写图片描述

在上一篇文章中所准备的源码中找到对应的文件src/rumtime/panic.go:425,即panic函数具体实现如下:

// The implementation of the predeclared function panic.
func gopanic(e interface{}) {gp := getg()    // getg()返回当前协程的 g 结构体指针,g 结构体描述 goroutineif gp.m.curg != gp {print("panic: ")printany(e)print("\n")throw("panic on system stack")}// m.softfloat is set during software floating point.// It increments m.locks to avoid preemption.// We moved the memory loads out, so there shouldn't be// any reason for it to panic anymore.if gp.m.softfloat != 0 {gp.m.locks--gp.m.softfloat = 0throw("panic during softfloat")}if gp.m.mallocing != 0 {print("panic: ")printany(e)print("\n")throw("panic during malloc")}if gp.m.preemptoff != "" {print("panic: ")printany(e)print("\n")print("preempt off reason: ")print(gp.m.preemptoff)print("\n")throw("panic during preemptoff")}if gp.m.locks != 0 {print("panic: ")printany(e)print("\n")throw("panic holding locks")}var p _panicp.arg = ep.link = gp._panicgp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))atomic.Xadd(&runningPanicDefers, 1)for {d := gp._defer    // 获取当前协程defer链表的头节点if d == nil {break    // 当前协程的defer都被执行后,defer链表为空,此时退出for循环}// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),// take defer off list. The earlier panic or Goexit will not continue running.if d.started {    // 发生panic后,在defer中又遇到panic(),则会进入这个代码块if d._panic != nil {d._panic.aborted = true}d._panic = nild.fn = nilgp._defer = d.linkfreedefer(d)  // defer 已经被执行过,则释放这个defer,继续for循环。continue}// Mark defer as started, but keep on list, so that traceback// can find and update the defer's argument frame if stack growth// or a garbage collection happens before reflectcall starts executing d.fn.d.started = true// Record the panic that is running the defer.// If there is a new panic during the deferred call, that panic// will find d in the list and will mark d._panic (this panic) aborted.d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))p.argp = unsafe.Pointer(getargp(0))reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))   // 执行当前协程defer链表头的deferp.argp = nil// reflectcall did not panic. Remove d.if gp._defer != d {throw("bad defer entry in panic")}d._panic = nild.fn = nilgp._defer = d.link  // 从defer链中移除刚刚执行过的defer// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic//GC()pc := d.pcsp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copyfreedefer(d)   // 释放刚刚执行过的deferif p.recovered {    // defer()中遇到recover后进入这个代码块atomic.Xadd(&runningPanicDefers, -1)gp._panic = p.link// Aborted panics are marked but remain on the g.panic list.// Remove them from the list.for gp._panic != nil && gp._panic.aborted {gp._panic = gp._panic.link}if gp._panic == nil { // must be done with signalgp.sig = 0}// Pass information about recovering frame to recovery.gp.sigcode0 = uintptr(sp)gp.sigcode1 = pcmcall(recovery)   // 跳转到recover()处,继续往下执行throw("recovery failed") // mcall should not return}}// ran out of deferred calls - old-school panic now// Because it is unsafe to call arbitrary user code after freezing// the world, we call preprintpanics to invoke all necessary Error// and String methods to prepare the panic strings before startpanic.preprintpanics(gp._panic)startpanic()// startpanic set panicking, which will block main from exiting,// so now OK to decrement runningPanicDefers.atomic.Xadd(&runningPanicDefers, -1)printpanics(gp._panic)   // 输出panic信息dopanic(0)       // should not return*(*int)(nil) = 0 // not reached
}

上面代码虽然有些没有看懂,但是其执行流程还是比较清楚,从代码上来看,协程遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中,遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。从执行顺序上来看,实际上是按照先进后出的顺序执行defer。这个时候应该会理解上面的面试题答案为什么是那样了。

这篇关于【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/318571

相关文章

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法

《golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法》:本文主要介绍golang获取当前时间、时间戳和时间字符串及它们之间的相互转换,本文通过实例代码给大家介绍的非常详细,感兴趣... 目录1、获取当前时间2、获取当前时间戳3、获取当前时间的字符串格式4、它们之间的相互转化上篇文章给大家介

使用Python开发一个简单的本地图片服务器

《使用Python开发一个简单的本地图片服务器》本文介绍了如何结合wxPython构建的图形用户界面GUI和Python内建的Web服务器功能,在本地网络中搭建一个私人的,即开即用的网页相册,文中的示... 目录项目目标核心技术栈代码深度解析完整代码工作流程主要功能与优势潜在改进与思考运行结果总结你是否曾经

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

Mysql表的简单操作(基本技能)

《Mysql表的简单操作(基本技能)》在数据库中,表的操作主要包括表的创建、查看、修改、删除等,了解如何操作这些表是数据库管理和开发的基本技能,本文给大家介绍Mysql表的简单操作,感兴趣的朋友一起看... 目录3.1 创建表 3.2 查看表结构3.3 修改表3.4 实践案例:修改表在数据库中,表的操作主要

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

springboot简单集成Security配置的教程

《springboot简单集成Security配置的教程》:本文主要介绍springboot简单集成Security配置的教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录集成Security安全框架引入依赖编写配置类WebSecurityConfig(自定义资源权限规则

golang 日志log与logrus示例详解

《golang日志log与logrus示例详解》log是Go语言标准库中一个简单的日志库,本文给大家介绍golang日志log与logrus示例详解,感兴趣的朋友一起看看吧... 目录一、Go 标准库 log 详解1. 功能特点2. 常用函数3. 示例代码4. 优势和局限二、第三方库 logrus 详解1.

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用