我正在参加「掘金·启航计划」
最近在网上看到下面一个考察 defer 使用的面试题。我们都知道 defer 就是在函数返回前执行的延迟函数,常用来关闭流、通道以及断开数据库连接。我们在调用 defer 时需要分析对应使用场景和机制。
package main
import "fmt"
type temp struct{}
func (t *temp) Add(elem int) *temp {
fmt.Println(elem)
return &temp{}
}
func main() {
tt := &temp{}
defer tt.Add(1).Add(2)
tt.Add(3)
}
首先提出两个问题:
1.defer 是什么时候调用的,其执行顺序应该是怎样的?
2.defer 是如何读取参数的?又是怎么返回参数的值的?
defer 调用机制
func a() {
i := 0
defer fmt.Println(i)
i++
}
运行以上代码,控制台输出:
0
我们都知道 golang 函数是值传递的,显然在调用 defer 延迟函数时,就立刻拷贝上一行代码执行的参数,于是输出为 0,而这里实际上 defer 拷贝不是值,而是函数指针,指向的 0 的那个地址。
记住“最后一个最先出”
func deferInloop() {
for i :=0 ; i< 5; i++ {
defer fmt.Println(i)
}
}
运行结果如下:
4
3
2
1
0
分析一下,下面的例子执行输出是什么?
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
// ☆☆☆ x = 0
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1()) // 5
fmt.Println(f2()) // 6
fmt.Println(f3()) // 5
fmt.Println(f4()) // 5
}
你分析对了吗? 这里最具迷惑的就是 f4()函数。别急,下面我们一起来分析下。
defer 调用原理
这里我们将 deferInloop() 这个函数生成汇编代码进行分析与解构。
CALL runtime.deferproc(SB)
TESTL AX, AX
JNE 139
JMP 28
CALL runtime.deferreturn(SB)
MOVQ 32(SP), BP
ADDQ $40, SP
RET
在编译器运行时 defer 延时函数分别调用 runtime 的 deferproc() 和 deferreturn()
func deferproc(fn func())
// 创建一个新的不带参数和结果的延时函数fn;编译器会在调用defer时转换成对该函数的调用。
func deferproc(fn func()) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
d := newdefer()
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
// 我们不能在调用getcallersp和将其存储到d.sp之间被抢占,因为getcallersp的结果是一个uintptr堆栈指针。
d.sp = getcallersp()
// deferproc正常返回0。停止panic的延迟func使deferproc返回1。编译器生成的代码总是检查返回值,如果deferproc返回,则跳转到函数的末尾!= 0。
return0()
}
该函数主要是创建一个 runtime._defer
结构体,赋值相关参数到对应的内容中。重点来了,这里调用了 newdefer
这个函数。下面我们先跳到对应的源码进一步分析。
func newdefer() *_defer
// 每个 P 都持有一个为 defer使用的池。
// 分配一个defer, 通常会占用per-P 池。
// 每个defer 都必须用 freedefer进行释放。
func newdefer() *_defer {
var d *_defer
mp := acquirem()
pp := mp.p.ptr()
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
releasem(mp)
mp, pp = nil, nil
if d == nil {
// Allocate new defer.
d = new(_defer)
}
d.heap = true
return d
}
该函数功能就是获取 _defer
结构体,其中主要通过三种方式来取出:
1. 当调度器的 defer 缓存池`ched.deferpool`存在,则取出放到当前 Goroutine 的defer缓存池 `pp.deferpool`中;
2. 当前 Goroutine的defer缓存池 `pp.deferpool`存在,则截取n-1个前的`pp.deferpool`;
3. 上面两种情况都不存在时,直接在堆上创建一个新的_defer 结构体。
执行该方法后我们知道当前的Goroutine 的_defer 链最前面会放入得到的_defer
,在执行时最后的_defer
就会被最先执行。_deferreturn
函数的执行过程就是取出Goroutine 上 _defer
链表最前面的_defer
的结构体,调用deferreturn
函数,具体源码如下:
func deferreturn()
// deferreturn 函数运行延迟函数给调用方, 编译器会在defer函数末尾插入该函数进行调用。
func deferreturn() {
gp := getg()
for {
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}
上面会判断Goroutine中 _defer
链表是否都执行了_defer
结构体,执行完成后则释放掉。
通过对源码的分析,我们不难分析出在文章开头的那个面试题的执行顺序。其顺序是tt.Add(1)
, tt.Add(3)
, tt.Add(2)
,即输出:1 3 2
.