前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 ><Go语言学习笔记>【异常处理】

<Go语言学习笔记>【异常处理】

原创
作者头像
Porco1Rosso
修改2020-11-19 18:15:17
1.5K0
修改2020-11-19 18:15:17
举报

关键字 Panic 和 Recover

通常 panicrecover 是用来处理异常问题的。我们来综述下,他们各自的特点:

painc 可以是系统出现严重错误时产生,也可以人为调用painc函数;如果不加处理,painc会沿着调用栈层层上报,直到程序崩溃终止。

recover 可以回收 panic,返回一个空接口,但是必须要在defer 中才行。

Panic 的细节与注意事项

不会影响其他Goroutine
代码语言:txt
复制
func main(){
	defer println("in main")
	go func() {
		defer println("in goroutine")
		panic("panic")
	}()
	time.Sleep(1 * time.Second)
}
in goroutine
panic: panic
...
exit status 2 //panic 一般都是错误码 2

可以注释掉这些defer ,看下他的打印结果,很直观。

在Goroutine中产生了panic,只会执行当前Goroutine中的defer方法,不会触发main中的(实际上,按照官方文档的解释,go func() 本身就是这个Goroutine 的main 函数)。如果把Goroutine中的defer方法注释掉,依然不会触发main中的defer,系统还是会崩溃。原因是:defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。

Panic的结构

_panic

代码语言:txt
复制
//源代码在 go/src/runtime/runtime2.go 899行
type _panic struct {
	argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
	arg       interface{}    // argument to panic
	link      *_panic        // link to earlier panic
	pc        uintptr        // where to return to in runtime if this panic is bypassed
	sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
	recovered bool           // whether this panic is over
	aborted   bool           // the panic was aborted
	goexit    bool
}

根据注释,我们大致可以清楚:

  • Link 会指向最近的panic,形成一个链表
  • recovered 和 aborted 是两个标记位
  • pc 和 sp 参与到goroutine相关
运行流程

编译器会把painc 转化为 gopanic 函数。具体的逻辑会在这个方法里执行。我这里只把里面的部分代码拿出来。这里面有非常详细的注释,很方便阅读。

代码语言:txt
复制
// 代码在 go/src/runtime/panic.go 887 行 
func gopanic(e interface{}) {
	gp := getg()

	//创建一个新的painc 并把他加到链表的最前端
	var p _panic
	p.arg = e
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	//循环defer链表, 并调用延迟函数。
	for {
		d := gp._defer
		...
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
		...
		freedefer(d)
		if p.recovered {
			...
		}
		...
	}

	...
	fatalpanic(gp._panic) // 最终处理方法

}

接下来,看下fatalpanic函数的情况:

代码语言:txt
复制
// 代码在 go/src/runtime/panic.go 1187 行 
func fatalpanic(msgs *_panic) {
	pc := getcallerpc()
	sp := getcallersp()
	gp := getg()
	var docrash bool

	systemstack(func() {
		if startpanic_m() && msgs != nil {
			atomic.Xadd(&runningPanicDefers, -1)
			printpanics(msgs) //打印出全部的panic信息,包括调用信息
		}
		docrash = dopanic_m(gp, pc, sp) //这里这方法很迷惑,不过看起来是针对goruntine 进行的操作
	})

	if docrash {
		crash()
	}

	systemstack(func() {
		exit(2) //如果panic没有被恢复,那么就会在这里退出程序,错误码 2
	})
}

Recover 的细节与注意事项

Recover 函数比较简单,这个东西就是用来处理Panic的没有其他的用途。我们直接看下源码:

代码语言:txt
复制
// 代码在 go/src/runtime/panic.go 1082 行 
func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		//这里只有存在Painc的时候,才会运行。
		p.recovered = true 
		return p.arg
	}
	//如果当前goruntine里没有panic 直接返回nil
	return nil
}

这里的逻辑是把_panic里的recovered设置成true,没有多余操作。之后回到gopanic函数中,经过简单处理跳转到recovery中。再往后就是 defer的逻辑,一直走到deferreturn,回到正常的逻辑中。

到这里基本可以总结一下Panic和Recover的一些特点。

  • Panic 是成链状保存的,并且Panic 只会触发自己所在的Goroutine中的Defer函数。
  • Recover函数只能在Defer中,并且存在一个Painc的时候才会生效,这个在他的源代码有体现。他处理的事情非常简单,就是把Panic的recovered属性设置为true。
  • 执行Panic的是runtime.gopanic函数。在这里,他依赖Defer,也会响应Recover的操作。在异常被恢复情况下,会一致走到deferreturn中,最终恢复现场。否则,会执行fatalpanic函数,打印出调用栈和异常信息,最后系统退出,错误码2。

通过学习源码,基本上解释了“panic 详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。”

关键字 Defer

Defer,人如其名,延迟执行函数。延迟到什么时候呢?这要延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么。注意:

  • 被延迟执行的是defer函数,而不是defer语句。简单的讲,函数预处理(我们稍后说明这种情况)
  • 被延迟的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。(一定是当前函数)
  • 在同一个函数中,defer函数调用的执行顺序与它们分别所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。

底层原理

Defer的底层原理非常非常的复杂,我们这里只做一些简单描述和调用逻辑说明,具体的过程可以参看最后的连接或者直接阅读源码。

数据结构说明
代码语言:txt
复制
//源代码在 go/src/runtime/runtime2.go 861行
type _defer struct {
	siz     int32 //参数和返回结果的内存大小
	started bool
	heap    bool
	openDefer bool //是否开放编码优化
	sp        uintptr  //栈指针计数器
	pc        uintptr  //程序计数器
	fn        *funcval  //实际延时的函数体 可以是空值
	_panic    *_panic  //触发defer 的panic 可以是空的
	link      *_defer //指向下一个defer
	//在开放编码优化的情况下,会使用到这两个字段
	fd   unsafe.Pointer 
	varp uintptr      
	//在堆栈模式下,会用到这个字段  
	framepc uintptr
}

通过观察,首先可以确认defer 仍然是一个链表结构,会通过link串起来。其次,直观的反映了defer 优化过程:

  • v1.13 以前的版本,采用堆上分配,性能较差
  • v1.13 引入栈上分配,增加效率
  • v1.14 引入了开放编码,进一步提高效率
调用逻辑说明

这一部分的逻辑非常复杂,建议看下 GO语言设计实现,写的很清楚,配合源码使用。

我们在这里只取过程和结论:

堆上分配 · 1.1 ~ 1.12
栈上分配 · 1.13
开放编码 · 1.14 ~ 现在
  • 编译期间判断 defer 关键字、return 语句的个数(defer不超过8个,defer*return 不超过15个)确定是否开启开放编码优化;
  • 通过 deferBits 和 cmd/compile/internal/gc.openDeferInfo 存储 defer 关键字的相关信息;
  • 如果 defer 关键字的执行可以在编译期间确定,会在函数返回前直接插入相应的代码,否则会由运行时的 runtime.deferreturn 处理;

问题引申,堆上分配和栈上分配的区别?GO 语言逃逸分析看内存分配

一些现象与说明

代码语言:txt
复制
func main(){
	a := 1
	b := 2
	defer calc(a, calc(a,b,"0"),"1")
	a = 0
	defer calc(a, calc(a,b,"3"),"2")
}

func calc(x,y int,s string) int{
	fmt.Println(s)
	fmt.Println(x,y,x+y)
	return x+y
}

返回的结果:

代码语言:txt
复制
0
1 2 3
3
0 2 2
2
0 2 2
1
1 3 4

这里考察两个点:

  1. Defer是栈调用,后写的先执行(先入后出)
  2. Defer的函数调用语句会在父函数调用后执行,但是用到的参数会在当时就执行得出(预计算)

这个现象就充分解释了一开始说的关于defer的知识点。关于第二点说的详细一下:

调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算

一些问题

Go 的异常处理与 try…catch…finally 的区别

这里直接引用 郝林的回复:

这是两种完全不同的异常处理机制。Go语言的异常处理机制是两层的,defer和recover可以处理意外的的异常,而error接口及相关体系处理可预期的异常。Go语言把不同种类的异常完全区别对待,我觉得这是一个进步。 另外,defer机制能够处理的远不止异常,还有很多资源回收的任务可以用到它。defer机制和goroutine机制一样,是一种很有效果的创新。 我认为defer机制正是建立在goroutine机制之上的。因为每个函数都有可能成为go函数,所以必须要把异常处理做到函数级别。可以看到,defer机制和error机制都是以函数为边界的。前者在函数级别上阻止会导致非正常控制流的意外异常外溢,而后者在函数级别上用正常的控制流向外传递可预期异常。 不要说什么先驱,什么旧例,世界在进步,技术更是在猛进。不要把思维固化在某门或某些编程语言上。每种能够流行起来的语言都会有自己独有的、已经验证的语法、风格和哲学。

能不能在defer中触发Panic

答案是当然可以了,如果理解了上边的原理的话,就能详细解释了,panic是链状的,后触发的panic会添加到最前端,循环调用defer的时候就会处理最前面的那个panic(可以理解为后发先至)。出现的现象就是,defer外的panic会被里面的替换掉。同样的,你甚至可以在defer中调用defer!

代码语言:txt
复制
func main(){
	defer func() {
		if p := recover(); p != nil {
			fmt.Printf("panic: %s\n", p) }
	}()
	defer func() {
		//defer func() {
			//if p := recover(); p != nil {
			//	fmt.Printf("panic: %s\n", p) }
		//}()
		panic("panic in defer")
	}()
	panic("panic in main")
}

https://blog.golang.org/defer-panic-and-recover

Go 语言 panic 和 recover 的原理 | Go 语言设计与实现

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 关键字 Panic 和 Recover
    • Panic 的细节与注意事项
      • 不会影响其他Goroutine
      • Panic的结构
      • 运行流程
    • Recover 的细节与注意事项
    • 关键字 Defer
      • 底层原理
        • 数据结构说明
        • 调用逻辑说明
        • 堆上分配 · 1.1 ~ 1.12
        • 栈上分配 · 1.13
        • 开放编码 · 1.14 ~ 现在
      • 一些现象与说明
      • 一些问题
        • Go 的异常处理与 try…catch…finally 的区别
          • 能不能在defer中触发Panic
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档