panic源码解读

前言

本文是在go version go1.13.15 darwin/amd64上进行的

panic的作用

  • panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine中递归执行调用方的defer

  • recover可以中止panic造成的程序崩溃。它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用;

举个栗子

package main

import "fmt"

func main() {
fmt.Println(1)
func() {
fmt.Println(2)
panic("3")
}()
fmt.Println(4)
}

输出

1
2
panic: 3 goroutine 1 [running]:
main.main.func1(...)
/Users/yj/Go/src/Go-POINT/panic/main.go:9
main.main()
/Users/yj/Go/src/Go-POINT/panic/main.go:10 +0xee

panic后会立刻停止执行当前函数的剩余代码,所以4没有打印出来

对于recover

  • panic只会触发当前Goroutine的defer;

  • recover只有在defer中调用才会生效;

  • panic允许在defer中嵌套多次调用;

package main

import (
"fmt"
"time"
) func main() {
fmt.Println(1) defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}() go func() {
fmt.Println(2)
panic("3")
}()
time.Sleep(time.Second)
fmt.Println(4)
}

上面的栗子,因为recoverpanic不在同一个goroutine中,所以不会捕获到

嵌套的demo

func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("3 panic again and again")
}()
panic("2 panic again")
}() panic("1 panic once")
}

输出

in main
panic: 1 panic once
panic: 2 panic again
panic: 3 panic again and again goroutine 1 [running]:
...

多次调用panic也不会影响defer函数的正常执行,所以使用defer进行收尾工作一般来说都是安全的。

panic使用场景

  • error:可预见的错误

  • panic:不可预见的异常

需要注意的是,你应该尽可能地使用error,而不是使用panicrecover。只有当程序不能继续运行的时候,才应该使用panicrecover机制。

panic有两个合理的用例。

1、发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。

2、发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic。

总结下panic的使用场景:

  • 1、空指针引用

  • 2、下标越界

  • 3、除数为0

  • 4、不应该出现的分支,比如default

  • 5、输入不应该引起函数错误

看下实现

先来看下_panic的结构

// _panic 保存了一个活跃的 panic
//
// 这个标记了 go:notinheap 因为 _panic 的值必须位于栈上
//
// argp 和 link 字段为栈指针,但在栈增长时不需要特殊处理:因为他们是指针类型且
// _panic 值只位于栈上,正常的栈指针调整会处理他们。
//
//go:notinheap
type _panic struct {
argp unsafe.Pointer // panic 期间 defer 调用参数的指针; 无法移动 - liblink 已知
arg interface{} // panic的参数
link *_panic // link 链接到更早的 panic
recovered bool // panic是否结束
aborted bool // panic是否被忽略
}

link指向了保存在goroutine链表中先前的panic链表

gopanic

编译器会将panic装换成gopanic,来看下执行的流程:

1、创建新的runtime._panic并添加到所在Goroutine的_panic链表的最前面;

2、在循环中不断从当前Goroutine 的_defer中链表获取runtime._defer并调用runtime.reflectcall运行延迟调用函数;

3、调用runtime.fatalpanic中止整个程序;

// 预先声明的函数 panic 的实现
func gopanic(e interface{}) {
gp := getg()
// 判断在系统栈上还是在用户栈上
// 如果执行在系统或信号栈时,getg() 会返回当前 m 的 g0 或 gsignal
// 因此可以通过 gp.m.curg == gp 来判断所在栈
// 系统栈上的 panic 无法恢复
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
// 如果正在进行 malloc 时发生 panic 也无法恢复
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
// 在禁止抢占时发生 panic 也无法恢复
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
// 在 g 锁在 m 上时发生 panic 也无法恢复
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
} // 下面是可以恢复的
var p _panic
p.arg = e
// panic 保存了对应的消息,并指向了保存在 goroutine 链表中先前的 panic 链表
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) atomic.Xadd(&runningPanicDefers, 1) for {
// 开始逐个取当前 goroutine 的 defer 调用
d := gp._defer
// 没有defer,退出循环
if d == nil {
break
} // 如果 defer 是由早期的 panic 或 Goexit 开始的(并且,因为我们回到这里,这引发了新的 panic),
// 则将 defer 带离链表。更早的 panic 或 Goexit 将无法继续运行。
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
} // 将deferred标记为started
// 如果栈增长或者垃圾回收在 reflectcall 开始执行 d.fn 前发生
// 标记 defer 已经开始执行,但仍将其保存在列表中,从而 traceback 可以找到并更新这个 defer 的参数帧 // 标记defer是否已经执行
d.started = true // 记录正在运行的延迟的panic。
// 如果在延迟调用期间有新的panic,那么这个panic
// 将在列表中找到d,并将标记d._panic(此panic)中止。
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))
p.argp = nil // reflectcall没有panic。删除d
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC() pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1) gp._panic = p.link
// 忽略的 panic 会被标记,但仍然保留在 g.panic 列表中
// 这里将它们移出列表
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // 必须由 signal 完成
gp.sig = 0
}
// 传递关于恢复帧的信息
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 调用 recover,并重新进入调度循环,不再返回
mcall(recovery)
// 如果无法重新进入调度循环,则无法恢复错误
throw("recovery failed") // mcall should not return
}
} // 消耗完所有的 defer 调用,保守地进行 panic
// 因为在冻结之后调用任意用户代码是不安全的,所以我们调用 preprintpanics 来调用
// 所有必要的 Error 和 String 方法来在 startpanic 之前准备 panic 字符串。
preprintpanics(gp._panic) fatalpanic(gp._panic) // 不应该返回
*(*int)(nil) = 0 // 无法触及
} // reflectcall 使用 arg 指向的 n 个参数字节的副本调用 fn。
// fn 返回后,reflectcall 在返回之前将 n-retoffset 结果字节复制回 arg+retoffset。
// 如果重新复制结果字节,则调用者应将参数帧类型作为 argtype 传递,以便该调用可以在复制期间执行适当的写障碍。
// reflect 包传递帧类型。在 runtime 包中,只有一个调用将结果复制回来,即 cgocallbackg1,
// 并且它不传递帧类型,这意味着没有调用写障碍。参见该调用的页面了解相关理由。
//
// 包 reflect 通过 linkname 访问此符号
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)

梳理下流程

1、在处理panic期间,会先判断当前panic的类型,确定panic是否可恢复;

  • 系统栈上的panic无法恢复
  • 如果正在进行malloc时发生panic也无法恢复
  • 在禁止抢占时发生panic也无法恢复
  • 在g锁在m上时发生panic也无法恢复

2、可恢复的panicpaniclink指向goroutine链表中先前的panic链表;

3、循环逐个获取当前goroutinedefer调用;

  • 如果defer是由早期panic或Goexit开始的,则将defer带离链表,更早的panic或Goexit将无法继续运行,也就是将之前的panic终止掉,将aborted设置为true,在下面执行recover时保证goexit不会被取消;

  • recovered会在gorecover中被标记,见下文。当recovered被标记为true时,recovery函数触发Goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值;

  • 当延迟函数中recover了一个panic时,就会返回1,当runtime.deferproc函数的返回值是1时,编译器生成的代码会直接跳转到调用方函数返回之前并执行runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。而runtime.gorecover函数也能从runtime._panic结构中取出了调用panic时传入的arg参数并返回给调用方。

// 在发生 panic 后 defer 函数调用 recover 后展开栈。然后安排继续运行,
// 就像 defer 函数的调用方正常返回一样。
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1 // d's arguments need to be in the stack.
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
} // 使 deferproc 为此 d 返回
// 这时候返回 1。调用函数将跳转到标准的返回尾声
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}

recovery函数中,利用g中的两个状态码回溯栈指针sp并恢复程序计数器pc到调度器中,并调用gogo重新调度g,将g恢复到调用recover函数的位置,goroutine继续执行,recovery在调度过程中会将函数的返回值设置为1。调用函数将跳转到标准的返回尾声。

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
... // deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}

当延迟函数中recover了一个panic时,就会返回1,当runtime.deferproc函数的返回值是1时,编译器生成的代码会直接跳转到调用方函数返回之前并执行runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。而runtime.gorecover函数也能从runtime._panic结构中取出了调用panic时传入的arg参数并返回给调用方。

gorecover

编译器会将recover装换成gorecover

如果recover被正确执行了,也就是gorecover,那么recovered将被标记成true

// go/src/runtime/panic.go
// 执行预先声明的函数 recover。
// 不允许分段栈,因为它需要可靠地找到其调用者的栈段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// 必须在 panic 期间作为 defer 调用的一部分在函数中运行。
// 必须从调用的最顶层函数( defer 语句中使用的函数)调用。
// p.argp 是最顶层 defer 函数调用的参数指针。
// 比较调用方报告的 argp,如果匹配,则调用者可以恢复。
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
// 标记recovered
p.recovered = true
return p.arg
}
return nil
}

在正常情况下,它会修改runtime._panicrecovered字段,runtime.gorecover函数中并不包含恢复程序的逻辑,程序的恢复是由runtime.gopanic函数负责。

gorecoverrecovered标记为true,然后gopanic就可以通过mcall调用recovery并重新进入调度循环

fatalpanic

runtime.fatalpanic实现了无法被恢复的程序崩溃,它在中止程序之前会通过runtime.printpanics打印出全部的panic消息以及调用时传入的参数:

// go/src/runtime/panic.go
// fatalpanic 实现了不可恢复的 panic。类似于 fatalthrow,
// 如果 msgs != nil,则 fatalpanic 仍然能够打印 panic 的消息
// 并在 main 在退出时候减少 runningPanicDeferss
//
//go:nosplit
func fatalpanic(msgs *_panic) {
// 返回程序计数寄存器指针
pc := getcallerpc()
// 返回堆栈指针
sp := getcallersp()
// 返回当前G
gp := getg()
var docrash bool
// 切换到系统栈来避免栈增长,如果运行时状态较差则可能导致更糟糕的事情
systemstack(func() {
if startpanic_m() && msgs != nil {
// 有 panic 消息和 startpanic_m 则可以尝试打印它们 // startpanic_m 设置 panic 会从阻止 main 的退出,
// 因此现在可以开始减少 runningPanicDefers 了
atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs)
} docrash = dopanic_m(gp, pc, sp)
}) if docrash {
// 通过在上述 systemstack 调用之外崩溃,调试器在生成回溯时不会混淆。
// 函数崩溃标记为 nosplit 以避免堆栈增长。
crash()
}
// 从系统推出
systemstack(func() {
exit(2)
}) *(*int)(nil) = 0 // not reached
} // 打印出当前活动的panic
func printpanics(p *_panic) {
if p.link != nil {
printpanics(p.link)
print("\t")
}
print("panic: ")
printany(p.arg)
if p.recovered {
print(" [recovered]")
}
print("\n")
}

总结

引一段来自【panic 和recover】的总结

1、编译器会负责做转换关键字的工作;

  • 1、将panicrecover分别转换成runtime.gopanicruntime.gorecover

  • 2、将defer转换成runtime.deferproc函数;

  • 3、在调用defer的函数末尾调用runtime.deferreturn函数;

2、在运行过程中遇到runtime.gopanic方法时,会从Goroutine的链表依次取出runtime._defer结构体并执行;

3、如果调用延迟执行函数时遇到了runtime.gorecover就会将_panic.recovered标记成true并返回panic的参数;

  • 1、在这次调用结束之后,runtime.gopanic会从runtime._defer结构体中取出程序计数器pc和栈指针sp并调用runtime.recovery函数进行恢复程序;

  • 2、runtime.recovery会根据传入的pcsp跳转回runtime.deferproc

  • 3、编译器自动生成的代码会发现runtime.deferproc的返回值不为0,这时会跳回runtime.deferreturn并恢复到正常的执行流程;

4、如果没有遇到runtime.gorecover就会依次遍历所有的runtime._defer,并在最后调用runtime.fatalpanic中止程序、打印panic的参数并返回错误码2

参考

【panic 和 recover】https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/

【恐慌与恢复内建函数】https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/panic/

【Go语言panic/recover的实现】https://zhuanlan.zhihu.com/p/72779197

【panic and recover】https://eddycjy.gitbook.io/golang/di-6-ke-chang-yong-guan-jian-zi/panic-and-recover#yuan-ma

【翻了源码,我把 panic 与 recover 给彻底搞明白了】https://jishuin.proginn.com/p/763bfbd4ed8c

go中panic源码解读的更多相关文章

  1. go中waitGroup源码解读

    waitGroup源码刨铣 前言 WaitGroup实现 noCopy state1 Add Wait 总结 参考 waitGroup源码刨铣 前言 学习下waitGroup的实现 本文是在go ve ...

  2. etcd中watch源码解读

    etcd中watch的源码解析 前言 client端的代码 Watch newWatcherGrpcStream run newWatchClient serveSubstream server端的代 ...

  3. java中jdbc源码解读

    在jdbc中一个重要的接口类就是java.sql.Driver,其中有一个重要的方法:Connection connect(String url, java.util.Propeties info); ...

  4. go中errgroup源码解读

    errgroup 前言 如何使用 实现原理 WithContext Go Wait 错误的使用 总结 errgroup 前言 来看下errgroup的实现 如何使用 func main() { var ...

  5. 【原】Spark中Job的提交源码解读

    版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...

  6. HttpServlet中service方法的源码解读

    前言     最近在看<Head First Servlet & JSP>这本书, 对servlet有了更加深入的理解.今天就来写一篇博客,谈一谈Servlet中一个重要的方法-- ...

  7. AbstractCollection类中的 T[] toArray(T[] a)方法源码解读

    一.源码解读 @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { //size为集合的大小 i ...

  8. go 中 sort 如何排序,源码解读

    sort 包源码解读 前言 如何使用 基本数据类型切片的排序 自定义 Less 排序比较器 自定义数据结构的排序 分析下源码 不稳定排序 稳定排序 查找 Interface 总结 参考 sort 包源 ...

  9. Mybatis源码解读-SpringBoot中配置加载和Mapper的生成

    本文mybatis-spring-boot探讨在springboot工程中mybatis相关对象的注册与加载. 建议先了解mybatis在spring中的使用和springboot自动装载机制,再看此 ...

随机推荐

  1. scala函数至简原则是什么?

    1.return可以省略,Scala会使用函数体的最后一行代码作为返回值 2.如果函数体只有一行代码,可以省略花括号 3.返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略) 4.如果有 ...

  2. 开源OA办公平台搭建教程:O2OA+Arduino实现物联网应用(二)

    O2OA平台搭建 O2OA的开发环境非常简单,安装服务器后即可通过浏览器进行开发了和使用.具体可参考文档库中的其他文档,有比较详细的介绍,这里就不再赘述了. Arduino开发发环境搭建 安装Ardu ...

  3. kubernetes和docker----2.学习Pod资源

    Pod--k8s最基础的资源 我们想要的是单个容器只运行一个进程 然而有时我们需要多个进程协同工作,所以我们需要另外一种更加高级的结构将容器组合在一起---pod Pod 我们来看一个最基本的pod ...

  4. 基于docker部署skywalking实现全链路监控

    一.概述 简介 skywalking是一个开放源码的,用于收集.分析,聚合,可视化来自于不同服务和本地基础服务的数据的可观察的平台,skywalking提供了一个简单的方法来让你对你的分布式系统甚至是 ...

  5. 后端程序员之路 21、一个cgi的c++封装

    在"3.fastcgi.fastcgi++"中,我们了解了cgi,也尝试了fastcgi++,这里,再记录一种对fastcgi的封装. 1.cgi接口层    request_t ...

  6. CCF(通信网络):简单DFS+floyd算法

    通信网络 201709-4 一看到题目分析了题意之后,我就想到用floyd算法来求解每一对顶点的最短路.如果一个点和任意一个点都有最短路(不为INF),那么这就是符合的一个答案.可是因为题目超时,只能 ...

  7. C# webapi跨域

    C# webapi跨域   第一种在Web.config中<system.webServer>节点中配置(不支持多个域名跨域) 1 <httpProtocol> 2 <c ...

  8. 2020年HTML5考试模拟题整理(二)

    1.以下是HTML5新增的标签是: AA.<aside>B.<isindex> C. <samp>D.<s>2.以下不是HTML5的新增的标签是: BA ...

  9. Spring AOP的源码流程

    一.AOP完成日志输出 1,导入AOP模块 <dependency> <groupId>org.springframework</groupId> <arti ...

  10. js 检测当前浏览其类型

    需求:检测并打印当前使用的浏览器类型 <script type="text/javascript"> function getBrowser(){ const str ...