Go语言设计
defer
Go 语言中, defer
会在当前函数返回前执行传入的函数。经常被用于关闭文件、数据库连接等操作。
实际上,Go在编译时会对defer
做一些小小的改动,比如如下代码:
编译后:
deferproc()
会先在当前的goroutine
中注册我们传入的方法,然后runtime.deferreturn()
会将注册的函数执行。
每个goroutine
在运行时都会有一个对应的结构体runtime.g
。其中有一个字段指向_defer
链表头,其指向的是一个个_defer
结构体。新注册的defer
,会添加在该链表的头部。执行时也会从头开始执行注册的_defer
:
所以,我们后注册的defer
会先被调用。
让我们简单看一下_defer
的数据结构:
在Go1.12版本中,注册defer
时,会在堆上,为_defer
分配内存并存储注册的defer
。 实际上,Go语言会预分配不同大小的deferpool
,只有当没有空闲的或者大小合适的_defer
时,才会从堆上直接分配。这样可以避免频繁的堆分配与回收。
在Go1.13中,在编译阶段会增加一些局部变量:
将defer
信息保存到当前函数栈帧的局部变量中,然后通过runtime.deferprocStack()
将这个_defer()
注册到g._defer
中。以减少defer
在堆上的分配。对于如下的代码:
因为变量i
需要在运行时才能确定,所以无法在编译阶段注册defer
在Go1.14中采用了一种叫做open coded defer
的方式,直接在编译时将defer
调用函数的参数使用局部变量保存,然后根据df
变量在函数返回之前确定是否需要被调用。
这种方式将defer
直接展开再调用函数内,从而不需要创建_defer
结构体,而且不需要注册在g._defer
链表中。 但和1.13版本一样,对于一些需要在运行时确定参数的defer
注册,还是需要采用1.12中的方式调用defer
。 此外,如果在运行函数是执行了panic
,之后的代码将不会被执行。1.14版本中采用的是额外栈扫描的方式来发现并执行defer
。这无疑将消耗额外的性能,但通常defer
被使用的次数远高于panic
,所以总体来看有很高的性能提升。
异常处理
Go语言中,异常分为两类error
和panic
。
通常error
用于抛出程序中函数调用失败,以传递错误信息。panic
则被用来处理程序运行时较为严重的错误,如果不处理panic
将会停止程序的运行。
Go语言认为我们应该对每个可能的异常进行手动处理。
error
error
是Go中提供的一个接口:
在标准包errors
中提供了多种类型的实现,最常用的是未公开的errorString
:
通过errors.New()
可以直接返回一个errorString
,标准包中的fmt
将会直接调用error
的Error()
方法来格式化输出。以下是处理求两数的商异常的例子:
panic
panic
的调用用于比较严重的程序错误,如果不进行处理将直接停止程序的运行。
Go提供了recover
来恢复panic
,然后在执行完当前goroutine
的_defer
链中的函数后结束当前调用函数:
运行以上代码后输出:
值得注意的是,recover()
只能在defer
中调用。
我们发现,程序先输出了"A2"然后才输出恢复的panic信息。实际上,在调用panic
时,Go语言运行时会调用runtime.gopanic()
在当前goroutine
的_panic
链中添加一个panic
。 然后结束当前 goroutine
剩余代码的执行,直接执行当前goroutine
中的_defer
链。 在_defer
链中调用recover()
函数时,会在_panic
链中将第一个未处理的panic
标记为已恢复,然后在该defer
中的设置已处理的panic
指针。 执行recover
的函数正常返回以后,继续执行剩余的_defer
链。 将已经设置了panic
的defer
删除,并标记该panic
为丢弃。 然后用相同的方式执行_defer
链中的其余函数。 最后当_defer
执行完时,遍历_panic
链,输出剩余未被丢弃的panic
信息。
让我们通过一个例子来看一下panic
和recover
的运行逻辑。在此之前让我们先了解以下panic
的数据结构:
当执行到1
时,会调用gopanic()
在当前goroutine
的_defer
链已经添加了B2()
和B1()
,同时将panicA
也添加到了当前goroutine
的_panic
链中:
然后由于调用了panic
,结束之后代码的执行,即2
处的代码不会被执行。然后转而调用goroutine
的_defer
链,即调用A2()
。在A2()
中又在当前goroutine
的_defer
链中添加了B1()
,然后再次调用gopanic()
,将panicA2
添加到了当前goroutine
的_panic
链中:
此时开始执行B1()
,recover()
将_panic
链中的第一个panic
标记为恢复,然后打印输出:
当B1()
执行完,返回到上一次的panic
触发位置,即3
。此时会去检查B2()
在_panic
中创建的panicB2
,发现已经被恢复,将其从_panic
中移除:
此时A2()
也执行完,返回到A()
中的1
处,继续执行_defer
链中的A1()
:
最后遍历当前goroutine
中的_panic
,输出panic
信息。
最后更新于