Go 语言中有一个非常有用的保留字 defer,defer 语句可以调用一个函数,该函数的执行被推迟到包裹它的函数返回时执行。
defer 语句调用的函数,要么是因为包裹它的函数执行了 return 语句,到达了函数体的末端,要么是因为对应的 goroutine 发生了 panic。
在实际的 go 语言程序中,defer 语句可以代替其它语言中 try…catch… 的作用,也可以用来处理释放资源等收尾操作,比如关闭文件句柄、关闭数据库连接等。
1. 编译器编译 defer 过程
defer dosomething(x)
简单来说,执行 defer 语句,实际上是注册了一个稍后执行的函数,确定了函数名和参数,但不会立即调用,而是把调用过程推迟到当前函数 return 或者发生 panic 的时候。
我们先了解一下 defer 相关的数据结构。
1) struct _defer 数据结构
go 语言程序中每一次调用 defer 都生成一个 _defer 结构体。
type _defer struct {
siz int32 // 参数和返回值的内存大小
started boul
heap boul // 区分该结构是在栈上分配的,还是对上分配的
sp uintptr // sp 计数器值,栈指针;
pc uintptr // pc 计数器值,程序计数器;
fn *funcval // defer 传入的函数地址,也就是延后执行的函数;
_panic *_panic // panic that is running defer
link *_defer // 链表
}
我们默认使用了 go 1.13 版本的源代码,其它版本类似。
一个函数内可以有多个 defer 调用,所以自然需要一个数据结构来组织这些 _defer 结构体。_defer 按照对齐规则占用 48 字节的内存。在 _defer 结构体中的 link 字段,这个字段把所有的 _defer 串成一个链表,表头是挂在 Goroutine 的 _defer 字段。
_defer 的链式结构如下:
_defer.siz 用于指定延迟函数的参数和返回值的空间,大小由 _defer.siz 指定,这块内存的值在 defer 关键字执行的时候填充好。
defer 延迟函数的参数是预计算的,在栈上分配空间。每一个 defer 调用在栈上分配的内存布局如下图所示:
其中 _defer 是一个指针,指向一个 struct _defer 对象,它可能分配在栈上,也可能分配在堆上。
2) struct _defer 内存分配
以下是一个使用 defer 的范例,文件名为 test_defer.go:
package main
func doDeferFunc(x int) {
println(x)
}
func doSomething() int {
var x = 1
defer doDeferFunc(x)
x += 2
return x
}
func main() {
x := doSomething()
println(x)
}
编译以上代码,加上去除优化和内链选项:
go tool compile -N -l test_defer.go
导出汇编代码:
go tool objdump test_defer.o
我们看下编译成的二进制代码:
从汇编指令我们看到,编译器在遇到 defer 关键字的时候,添加了一些运行库函数:deferprocStack 和 deferreturn。
go 1.13 正式版本的发布提升了 defer 的性能,号称针对 defer 场景提升了 30% 的性能。
go 1.13 之前的版本 defer 语句会被编译器翻译成两个过程:回调注册函数过程:deferproc 和 deferreturn。
go 1.13 带来的 deferprocStack 函数,这个函数就是这个 30% 性能提升的核心手段。deferprocStack 和 deferproc 的目的都是注册回调函数,但是不同的是 deferprocStatck 是在栈内存上分配 struct _defer 结构,而 deferproc 这个是需要去堆上分配结构内存的。而我们绝大部分的场景都是可以是在栈上分配的,所以自然整体性能就提升了。栈上分配内存自然是比对上要快太多了,只需要改变 rsp 寄存器的值就可以进行分配。
那么什么时候分配在栈上,什么时候分配在堆上呢?
在编译器相关的文件(src/cmd/compile/internal/gc/ssa.go )里,有个条件判断:
func (s *state) stmt(n *Node) {
case ODEFER:
d := callDefer
if n.Esc == EscNever {
d = callDeferStack
}
}
n.Esc 是 ast.Node 的逃逸分析的结果,那么什么时候 n.Esc 会被置成 EscNever 呢?
这个在逃逸分析的函数 esc 里(src/cmd/compile/internal/gc/esc.go ) :
func (e *EscState) esc(n *Node, parent *Node) {
case ODEFER<