defer规则
讲到defer,首先需要了解它的使用规则,defer语句满足三个原则:
1. 延迟函数的参数在defer函数出现时就已经确定下来了
func test1() {
i := 0
defer fmt.Println(i)
i++
return
}
=> 打印结果为0
如上实验函数1,在defer出现时延迟函数fmt.Println
的参数i
的值就已经确定了,此时是0;所以即便defer后更新了i
的值,最终defer打印的结果仍然为0
func test2(){
i := map[string]int64{
"1": 1,
"2": 2,
}
defer fmt.Println(i)
i["3"] = 3
return
}
=> 打印结果为 map[1:1, 2:2, 3:3]
如上实验函数2, 因为此时i
为引用类型的变量map,所以defer语句出现时确定的是map的引用,后面map修改,defer执行时仍然会感知变化,即:
对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对 变量的修改可能会影响延迟函数。
2. 延迟函数执行按后进先出顺序执行,即先出现的 defer最后执行
defer函数的定义类似于入栈的操作,执行类似出栈操作,后进先出;
每个goroutine会维护一个defer链表用于执行defer函数
3. 延迟函数可能操作主函数的具名返回值
这个原则涉及到函数返回的过程。首先需要明确的是,函数返回return不是一个原子操作
func test3() (i int64) {
i = 0
defer func() { i++ }()
return i
}
fmt.Println(test3()) => 1
函数返回分为两步,以test3函数中return i
为例:
- 第一步,将
i
的值存入栈中作为返回值 - 第二部,执行跳转程序执行指令
ret
defer函数的执行正是处于第一步之后,第二部之前,所以defer可以修改具名的函数返回值
defer的实现原理
底层数据结构
type _defer struct {
siz int32 // defer函数参数大小
started bool
heap bool
openDefer bool
sp uintptr // 调用函数的sp
pc uintptr // 调用函数的pc
fn *funcval // defer延迟函数指针
_panic *_panic // 触发的panic信息
link *_defer // defer函数链表
}
defer的创建
defer的创建过程在runtime/panic.go
中的deferproc()
函数定义
// @titile: deferproc
// @description: 创建一个defer函数
// @param:
// siz: int32. 参数大小,字节数
// fn: *funcval. 函数指针
func deferproc(siz int32, fn *funcval) {
// 获取当前goroutine
gp := getg()
...
// getcallersp returns the stack pointer (SP) of its caller's caller
// 获取调用函数(调用defer的函数)的栈指针
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
// getcallerpc returns the program counter (PC) of its caller's caller
// 获取调用函数的程序计数器
callerpc := getcallerpc()
// 新建一个_defer对象
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
// 这两步链表操作,将新建的_defer对象放到goroutine的链表头
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = callerpc
d.sp = sp
// 处理一下参数,把指定大小的参数,复制到_defer对象对应的参数区
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
defer的执行
defer的执行过程在runtime/panic.go
中的deferreturn()
函数定义
// @title: deferreturn
// @description: defer执行函数(部分代码和deferproc一样,就不注释了)
// @param:
// arg0: uintptr. 存放延迟函数参数的地址
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
// 这个参数不太熟悉,暂时跳过
if d.openDefer {
...
}
// 把延迟函数的参数复制到args0中
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
// 把当前defer对象从链表中取出,goroutine直接指向当前defer指向的下一个_defer对象
// 创建_defer放到链表头,取出从表头取,实现后进先出的栈模型
gp._defer = d.link
// freedefer函数主要清空当前_defer对象的参数,保证其不能再被执行
freedefer(d)
// 执行延迟函数
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
从defer的创建和执行过程我们总结出一下几点:
- defer函数是注册在当前goroutine的
_defer
参数中,且通过链表的方式实现后进先出的栈模型- 同时说明,defer函数的作用域就在当前goroutine中,不能跨goroutine执行