go defer 变量作用域分析

662 阅读2分钟

go语言中defer用于在退出当前函数前执行某些特定逻辑,比如进行close/unlock动作,defer中函数入参传递和函数调用入参传递一样都是值传递,也就是会立即复制外部引用的入参。

defer结构体如下:

type _defer struct {
	siz       int32 // 参数和结果的内存大小;
	started   bool
	openDefer bool
	sp        uintptr
	pc        uintptr // 分别代表栈指针和调用方的程序计数器
	fn        *funcval // 关键字中传入的函数
	_panic    *_panic // 是触发延迟调用的结构体,可能为空;
	link      *_defer
}

所有defer函数都会通过通过link字段串联成链表(每个goroutinue有一个_defer链表,defer函数执行类似于栈结构的FIFO方式,最新defer的函数最先被调用): image.png

为什么defer要设计成类似栈结构的执行方式呢?这其实是和程序中函数调用执行机制-栈结构契合,如果不设计成栈调用方式,那么就需要记录或者从头遍历直到当前函数帧对应的defer函数才行,不如按照栈的FIFO机制来的简单。

goroutine 会保存 defer 调用链,函数 return 时候会判断 goroutine defer 链中的 sp 和当前 sp 关系,如果符合条件就执行 defer 结构体中保存的函数指针,参数也是通过 defer 结构体保存的传递过去的。

如果defer执行可以在编译期确定,会在函数return前直接插入对应的代码,否则会由runtime.returndefer来执行。

defer函数中变量有2类,一种是defer函数入参,另一种是defer函数内使用了外部变量。前者和普通的函数调用传递是一样的,这里不在细讲;后者其实就是闭包引用,不过只有到defer函数真正执行时才能根据上下文确定确定当前值(go闭包引用和Java中实现机制不一样,Java中引用外部变量后该外部变量就不能变更了,相当于final变量),因此defer中的闭包引用变量只有到函数return时才能知道到底是什么数据。

func main() {
   Func()
}
func Func() {
   a := 1
   defer func() {
      fmt.Println("first defer:", a)
   }()

   a = 2
   defer func() {
      fmt.Println("second defer:", a)
   }()

   defer func() {
      fmt.Println("third defer:", a)
      a = 3 // 更新a的值会影响到后续defer引用a
   }()
}

// 结果输出:
third defer: 2
second defer: 3
first defer: 3

return执行经过defer时会将return xxx这一句语句编译为如下3个语句:

返回值 = xxx
调用defer函数
空的return

因此,如果函数return某个指针变量并且defer中将其变更的话,会导致返回defer变更后的值,不太符合一般认知,对于非指针类则无影响。