defer,即延迟调用,是Go语言的一大特色。defer代码块会在函数调用链表中增加一个函数调用,在函数正常返回,即return返回之后,增加一个函数调用。因此,defer常用来回收资源,哪怕程序执行有错误,依然能够保证回收资源等操作能够执行。
它的使用有三条规则:
- 当defer被声明时,其参数就会被实时解析
- defer执行顺序,为先进后出
- defer可以读取有名返回值
那么这三条规则具体是什么含义呢,可以通过具体的例子来看:
func add(){
i := 10
defer fmt.Println(i)
i++
return
}
最终程序会输出10,这是因为虽然我们在defer后面跟了一个带变量的函数: fmt.Println(i). 但这个变量i在defer被声明的时候,就已经确定值了。即参数会被实时解析。因此这段代码等同于:
func add(){
i := 10
defer fmt.Println(10)
i++
return
}
继而可以看下面的例子:
func f1() (result int) {
defer func() {
result++
}()
return 0
}
func f2() (r int) {
t := 5
defer func() {
t = t+5
}()
return t
}
func f3() (t int) {
t = 5
defer func() {
t = t+5
}()
return t
}
func f4() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
这4个函数依次执行结果是1,5,10,1。函数返回的过程是先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。
因此,我们可以将return xxx改成 返回值=xxx 调用defer函数 return空 的形式,那上面的例子就可以改成:
func f11() (result int) {
result = 0 //先给返回值赋值
func(){ //再执行defer 函数
result++
}()
return //最后返回
}
func f22() (r int) {
t := 5
r = t //赋值指令
func() { //defer 函数被插入到赋值与返回之间执行,这个例子中返回值r没有被修改
t = t+5
}
return //返回
}
func f33() (t int) {
t = 5 //赋值指令
func(){
t = t+5 //然后执行defer函数,t值被修改
}
return
}
func f44() (r int) {
r = 1 //给返回值赋值
func(r int){ //这里的r传值进去的,是原来r的copy,不会改变要返回的那个r值
r = r+5
}(r)
return
}
规则2就比较简单,最直观的方式是把它放入for循环中,观察结果就能得知:
func f() {
for i := 1; i < 5; i++ {
defer fmt.Print(i)
}
}
运行结果为4321,先进后出的顺序~
func main() {
fmt.Println("a return:", a()) // 打印结果为 a return: 0
}
func a() int {
var i int
defer func() {
i++
fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
}()
defer func() {
i++
fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
}()
return i
}
a defer1: 1
a defer2: 2
a return: 0
规则3:
func f() (i int) {
defer func() { i++ }()
return 1
}
这里,函数定义了返回值i,最终return 1将i赋值为1,然后执行defer,进行自增操作,最终结果为2。虽然defer是在return之后执行,但是它的作用域仍然在函数之内。
可以总结一下,如果函数的返回值是无名的,那个golang会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作,而有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值)
下面再综合给出4个易错的例子,左下与左上和上面讲述的例子一致,右上先定义了局部变量i,但是返回值是无名的int,因此i++没啥作用,最后return i相当于return 10,即创建一个临时变量保存return的值,右下与右上进行对比,右下是指针操作,操作的是该临时变量的地址值,因此可以改变return的内容。
最后说一句,延迟调用的确是个好东西,但是他不是一个简单的操作,涉及到了对象的分配,缓存以及多次函数调用,因此在对性能要求非常高的场合,最好少用defer。
文中例子参考:https://blog.csdn.net/huang_yong_peng/article/details/82950743