[TOC]
每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行;如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.
defer语句并不会马上执行,而是会进入一个栈,函数return前,会按先后出的顺序执行。也说是说最先被定义的defer语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行,那后面函数的依赖就没有了。
在defer函数定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。作为函数参数,则在defer定义时就把值传递给defer,并被cache起来;作为闭包引用的话,则会在defer函数真正调用时根据整个上下文确定当前的值。
defer后面的语句在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个“值”,那么就和定义的时候是一致的。如果此变量是一个“引用”,那么就可能和定义的时候不一致。
func main() {
fmt.Println(increaseA()) // 0
fmt.Println(increaseB()) // 1
fmt.Println(f1()) // 10
fmt.Println(f2()) // 5
fmt.Println(f3()) // 0
fmt.Println(f4()) // 2
fmt.Println(f5()) // 2
}
func increaseA() int {
var i int
defer func() { i++ }()
return i
}
func increaseB() (r int) {
defer func() { r++ }()
return r
}
func f1() (r int) {
r = 5
defer func() { r = r + 5 }()
return r
}
func f2() (r int) {
t := 5
defer func() { t = t + 5 }()
return t
}
func f3() (r int) {
defer func(r int) { r = r + 5 }(r)
return r
}
func f4() (r int) {
i := 1
defer func() {
r++
}()
return i
}
func f5() (r int) {
defer func() {
r++
}()
return 1
}
return xxx 这条语句经过编译之后,变成了三条指令:
f2 拆解后:
func f2() (r int) {
t := 5
// 1. 赋值指令
r = t
// 2. defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
func() {
t = t + 5
}
// 3. 空的return指令
return
}
f4 拆解后:
func f4() (r int) {
i := 1
// 1. 赋值指令
r = i
defer func() {
r++
}()
// 3. 空的return指令
return
}
f5 拆解后:
func f5() (r int) {
// 1. 赋值指令
r = 1
defer func() {
r++
}()
// 3. 空的return指令
return
}
package main
import "fmt"
type number int
func (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {
var n number
defer n.print() //0
defer n.pprint() //3
defer func() { n.print() }() //3
defer func() { n.pprint() }() //3
n = 3
// output:
// 3
// 3
// 3
// 0
}
第四个defer语句是闭包,引用外部函数的n, 最终结果是3;
第三个defer语句同第四个;
第二个defer语句,n是引用,最终求值是3.
第一个defer语句,对n直接求值,开始的时候n=0, 所以最后是0;
延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// output:
// 3
// 2
// 1
延迟函数的参数在defer语句出现时就已经确定下来了
注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。
package main
import "fmt"
func main() {
i := 0
defer fmt.Println(i) //(0) 这也算是作为defer函数的参数
defer func(j int) { fmt.Println(j) }(i) //(0) 作为参数
defer func() { fmt.Println(i) }() //(1) 作为闭包(closure)进行引用
i++
}
// output:
// 1
// 0
// 0
package main
import "fmt"
type Person struct {
name string
}
func main() {
person := &Person{"Lilei"}
defer fmt.Println(person.name) // person.name作为普通类型当做defer函数的参数
defer fmt.Printf("%v\n", person) // 引用类型作为参数
defer func(p *Person) { fmt.Println(p.name) }(person) // 同上
defer func() { fmt.Println(person.name) }() // 闭包引用,对引用对象属性的修改不影响引用
person.name = "HanMeimei"
}
// output:
// HanMeimei
// HanMeimei
// &{HanMeimei}
// Lilei
package main
import "fmt"
type Person struct {
name string
}
func main() {
person := &Person{"Lilei"}
defer fmt.Println(person.name) // 同上,person.name作为普通类型当做defer函数的参数
defer fmt.Printf("%v\n", person) // 作为defer函数的参数,申明时指向“Lilei”
defer func(p *Person) { fmt.Println(p.name) }(person) // 同上
defer func() { fmt.Println(person.name) }() // 作为闭包引用,随着person的改变而指向“HanMeimei”
person = &Person{"HanMeimei"}
}
// output:
// HanMeimei
// Lilei
// &{Lilei}
// Lilei
延迟函数可能操作主函数的具名返回值
package main
import "fmt"
func main() {
fmt.Printf("output: %d\n", f())
}
func f() (i int) {
defer fmt.Println(i) // 参数引用
defer func(j int) { fmt.Println(j) }(i) // 同上
defer func() { fmt.Println(i) }() // 闭包引用
defer func() { i++ }() // 执行前,i=2
defer func() { i++ }() // 执行前,i=1
i++
return
}
output
3
0
0
output: 3
package main
import "fmt"
func main() {
var whatever [3]struct{}
for i := range whatever {
defer func() {
fmt.Println(i)
}()
}
}
// 2
// 2
// 2
源码包src/src/runtime/runtime2.go:_defer定义了defer的数据结构:
type _defer struct {
sp uintptr //函数栈指针
pc uintptr //程序计数器
fn *funcval //函数地址
link *_defer //指向自身结构的指针,用于链接多个defer
}
我们知道defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。
与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。
下图展示一个goroutine定义多个defer时的场景:
从上图可以看到,新声明的defer总是添加到链表头部。
函数返回前执行defer则是从链表首部依次取出执行,不再赘述。
一个goroutine可能连续调用多个函数,defer添加过程跟上述流程一致,进入函数时添加defer,离开函数时取出defer,所以即便调用多个函数,也总是能保证defer是按FIFO方式执行的。
源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。
可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。
return不是原子操作,执行过程是: 保存返回值(若有)-->执行defer(若有)-->执行ret跳转