我们还是从代码示例着手来了解Go语言的defer是如何实现的,首先我们看下案例一:
func main() {i := 0defer fmt.Println(f1(i))i = i + 2}func f1(i int) int {return i + 1}
案例一的输出是 1,我们再看案例二:
func main() {i := 0defer func() {fmt.Println(f1(i))}()i = i + 2}func f1(i int) int {return i + 1}
案例二的输出是3。
怎么解释上述输出的不同呢?我们先看下defer的定义:Go 语言的 defer
会在当前函数返回前执行传入的函数。在这里我们一般都关注了“会在当前函数返回器执行”而忽略了“函数”,所以我特别地把函数标红加粗。而在调用用 defer 关键字的时候,会立即拷贝调用该函数的参数。
在案例一中的“函数”是 fmt.Println(),其内部引用的参数是 f1方法的返回值。此时立刻把调用f1()函数的返回值 1 拷贝下来(此时main函数还未运行到第4行,i的值仍然为0,f1() 的返回值为1),在main方法返回前相当于调用fmt.Println(1),
而在案例二中“函数”是匿名函数,其内部引用的参数是该匿名函数的指针。当我们把案例二中的匿名函数命名,其代码类似如下:
import "fmt"func main() {i := 0f2 := func() {fmt.Println(f1(i))}defer f2()i = i + 2}func f1(i int) int {return i + 1}
此时 f2 函数是没有参数的,不需要对参数进行拷贝,而函数继续运行到最后 main 函数要退出前会调用f2方法,而此时i已经变成 2 了,再执行 f1() 返回的结果是3,所以案例二输出的结果是3。
defer关键字的结构定于在Go源码的runtime/runtime2.go文件中:
// A _defer holds an entry on the list of deferred calls.// If you add a field here, add code to clear it in freedefer and deferProcStack// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct// and cmd/compile/internal/gc/ssa.go:(*state).call.// Some defers will be allocated on the stack and some on the heap.// All defers are logically part of the stack, so write barriers to// initialize them are not required. All defers must be manually scanned,// and for heap defers, marked.type _defer struct {siz int32 // includes both arguments and resultsstarted boolheap bool// openDefer indicates that this _defer is for a frame with open-coded// defers. We have only one defer record for the entire frame (which may// currently have 0, 1, or more defers active).openDefer boolsp uintptr // sp at time of deferpc uintptr // pc at time of deferfn *funcval // can be nil for open-coded defers_panic *_panic // panic that is running deferlink *_defer// If openDefer is true, the fields below record values about the stack// frame and associated function that has the open-coded defer(s). sp// above will be the sp for the frame, and pc will be address of the// deferreturn call in the function.fd unsafe.Pointer // funcdata for the function associated with the framevarp uintptr // value of varp for the stack frame// framepc is the current pc associated with the stack frame. Together,// with sp above (which is the sp associated with the stack frame),// framepc/sp can be used as pc/sp pair to continue a stack trace via// gentraceback().framepc uintptr}
我们看到该结构中有个 link 字段指向调用链路的上一个 _defer。我们可以把Go的defer看成一个链表。

fn:指向调用函数的指针,案例一中的fmt.println(),案例三中的f2()。
siz:defer函数的入参和返回值空间大小,其作用表现在 _defer 紧跟着的siz空间存储fn函数的入参,和返回值。
heap:用来区分 _defer 在栈上分配还是在堆上分配。
其他参数有一部分在GC时使用的。
在Go1.3的Release Note中有这么一句话,其大概意思就是在1.3对defer提升了30%的效率。
This release improves performance of most uses of defer
by 30%.
我们来看下Go1.3相对于之前的版本对defer进行了那些优化的呢?我们来看下在Go1.3之后的版本中的一段源码:

可以看到在有些条件下栈上申请_defer空间,而有些时候在堆上申请空间,同时还有一个open-coded类型,这个我们在后文中介绍。在Go1.3以前_defer都是在堆上分配的,而在Go1.3以后可以在栈上分配_defer空间(绝大多数场景),所以其效率高很多。
那在栈上分配_defer空间的条件是什么呢?可以看代码是Esc==EscNever,我们再搜下代码EscNever怎样会被负值,可以搜到这段代码:

由于没有系统的去读这块代码,不清楚含义,但是我通过loopDepth来用塑料英语再翻译一下,表示循环的深度,难道就是循环的深度为1才在栈上分配_defer结构的内存?
然后我去网上搜了下资料,loopDepth确实是用来检测循环层次的,那也就是说嵌套的循环作用域里的_defer将不会被分配在栈上。 这种情况确实在使用defer的时候比较少见,所以绝大多数场景下的_defer将会分配到栈上。
那么,我们在使用defer的时候需要注意了:在使用defer时,其外层尽量不要使用嵌套的for循环,这样将会大大的降低defer的运行效率。
而我们在上文中除了在堆栈中分配_defer,还提到了open_coded,那这又是什么呢?这就是在Go1.4及其以后引入了开放编码。开放编码就是Go会在编译期间就把defer需要执行的代码逻辑直接插入到函数的尾部,则在运行时不需要再有defer什么事了。
open_coded在Go1.4中的引用同时也是对defer的性能进行了进一步的提升。而在苛刻的情况下才会启用open_coded,在下一节我们将详细讲解open_coded的开启和实现机制。
往期回顾:
go语言系列4 - Goroutine、channel的使用还有话说





