Golang源码阅读笔记 - Defer

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的创建和执行过程我们总结出一下几点:

  1. defer函数是注册在当前goroutine的_defer参数中,且通过链表的方式实现后进先出的栈模型
    • 同时说明,defer函数的作用域就在当前goroutine中,不能跨goroutine执行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值