在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。
通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。
defer的实现
defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。
在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。
普通的函数返回时,汇编代码类似:
add xx SP return
如果其中包含了defer语句,则汇编代码是:
call runtime.deferreturn, add xx SP return
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。
规则一 当defer被声明时,其参数就会被实时解析
我们通过以下代码来解释这条规则:
package main import "fmt" func main() { i := 0 defer fmt.Println(i) i++ return }
./hello 0
运行结果是0
这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。 换言之,上面的代码等同于下面的代码:
func a() { i := 0 defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作 i++ return }
为了更为明确的说明这个问题,我们继续定义一个defer:
package main import "fmt" func main() { i := 0 defer fmt.Println(i) //输出0,因为i此时就是0 i++ defer fmt.Println(i) //输出1,因为i此时就是1 return }
defer调用顺序
./hello 1 0
通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)
再看一个例子:
defer 和匿名函数
package main import "fmt" func f1() (result int) { fmt.Println("enter f1") defer func() { fmt.Println("defer f1") result++ //操作返回值 }() return 0 } func f2() (r int) { fmt.Println("enter f2") t := 5 defer func() { fmt.Println("defer f2") t = t+5 }() return t //返回10吗 } func f3() (t int) { fmt.Println("enter f3") t = 5 defer func() { fmt.Println("defer f3") t = t+5 // 操作外围返回值 }() return t // 返回10吗 } func f4() (r int) { fmt.Println("enter f4") defer func(r int) { fmt.Println("defer f4") r = r + 5 }(r) return 1 } func main() { fmt.Println(f1()) fmt.Println(f2()) fmt.Println(f3()) fmt.Println(f4()) }
root@ubuntu:~/go_learn/example.com/hello# ./hello
enter f1
defer f1
1
enter f2
defer f2
5 -------------不是10
enter f3
defer f3
10
enter f4
defer f4
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 }
规则二 defer执行顺序为先进后出
当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。不要为什么,golang就是这么定义的。我们用下面的代码加深记忆和理解:
package main import "fmt" func main() { for i := 0; i < 4; i++ { defer fmt.Println(i) } }
./hello 3 2 1 0
在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了3210.
再看之前的那个例子:
package main import "fmt" 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 }
./hello a defer1: 1 a defer2: 2 a return: 0
规则三 defer可以读取有名返回值
先看下面的代码:
package main import "fmt" func a() (i int) { defer func() { i++ }() return 1 } func main() { fmt.Println("a return:", a()) }
./hello a return: 2
输出结果是12. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢…)。
当执行return 1 之后,i的值就是1. 此时此刻,defer代码块开始执行,对i进行自增操作。 因此输出2.
package main import "fmt" func trace(s string) string { fmt.Println("entering:",s) return s } func un(s string) { fmt.Println("leaving:",s) } func a() { defer un(trace("a")) fmt.Println("in a") } func b() { defer un(trace("b")) fmt.Println("in b") a() } func main() { b() }
entering: b in b entering: a in a leaving: a leaving: b