defer 语句用于延迟函数的调用,使用 defer 关键字修饰一个函数,会将这个函数压入栈中,当函数返回时,再把栈中函数取出执行。
老规矩,我们先来答几道题试试水。
func deferTest() {
var a = 1
defer fmt.Println(a)
a = 2
return
}
答 案 是:1
解析:延迟函数 fmt.Println(a) 的参数在 defer 语句出现的时候就已经确定下来了,所以不管后面如何修改 a 变量,都不会影响延迟函数。
package main
import "fmt"
func main() {
deferTest()
}
func deferTest() {
var arr = [3]int{1, 2, 3}
defer printTest(&arr)
arr[0] = 4
return
}
func printTest(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
答 案 是:
4
2
3
解析:延迟函数 printTest() 的参数在 defer 语句出现的时候就已经确定下来了,即为数组的地址,延迟函数执行的时机是在 return 语句之前,所以对数组的最终修改的值会被打印出来。
package main
import "fmt"
func main() {
res := deferTest()
fmt.Println(res)
}
func deferTest () (result int) {
i := 1
defer func() {
result++
}()
return i
}
答 案 是:
2
解析:函数的 return 语句并不是原子级的,实际的执行过程为为设置返回值—>ret,defer 语句是在返回前执行,所以返回过程是:「设置返回值—>执行defer—>ret」。所以 return 语句先把 result 设置成 i
的值(1),defer 语句中又把 result 递增 1 ,所以最终返回值为 2 。
上面题目中我们已经了解到,函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。返回过程是:「设置返回值—>执行defer—>ret」。
func deferTest () (result int) {
i := 1
defer func() {
result++
}()
return i
}
上面有 defer 例子的return 语句实际执行过程是:
result = i
result++
return
当主函数有一个匿名返回值,返回时使用字面值,例如返回 “1”,“2”,“3” 这样的值,此时 defer 语句是不能操作返回值的。
func test() int {
var i int
defer func() {
i++
}()
return 1
}
上面的 return 语句,直接把1
作为返回值,延迟函数无法操作返回值,所以也就不能修改返回值。
当主函数有一个匿名返回值,返回会使用本地或者全局变量,此时 defer 语句可以引用到返回值,但不会改变返回值。
func test() int {
var i int
defer func() {
i++
}()
return i
}
上面的函数,返回一个局部变量,defer 函数也有操作这个局部变量。对于匿名返回值来说,我们可以假定仍然有一个变量用来存储返回值,例如假定返回值变量为 ”aaa”,上面的返回语句可以拆分成以下过程:
aaa = i
i++
return
由于i是整型,会将值拷贝给变量 aaa,所以defer语句中修改 i的值,对函数返回值不造成影响。
主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。
package main
import "fmt"
func main() {
res := test()
fmt.Println(res) // 1
}
func test() (i int) {
defer func() {
i++
}()
return 0
}
上面的返回语句可以拆分成以下过程:
i = 0
i++
return
源码包 src/src/runtime/runtime2.go:_defer 定义了defer的数据结构:
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer
// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}
defer 语句后面是要跟一个函数的,所以 defer 的数据结构跟一般的函数类似,不同之处是 defer 结构含有一个指针,用于指向另一个 defer ,每个 goroutine 数据结构中实际上也有一个 defer 指针指向一个 defer 的单链表,每次声明一个defer 时就将 defer 插入单链表的表头,每次执行 defer 时就从单链表的表头取出一个 defer 执行。保证 defer 是按 FIFO 方式执行的。
源码包 src/runtime/panic.go 中定义了两个方法分别用于创建defer和执行defer。
有什么问题,可以公众号内回复或加我微信交流。