defer衍生出来的各种用法让人眼花缭乱,但其实Go官方对defer的规则只有三条
规则一:延迟函数的参数在defer语句出现时就已经确定了
func func1() {
i := 0
defer fmt.Println(i)//输出0
i++
defer fmt.Println(i)//输出1
return
}
输出:
1
0
defer语句中的 fmt.Println() 参数i值在defer出现时就已经确定了,实际上是复制了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印0。
注意:对于指针类型的参数,此规则仍然适用,只不过延迟函数的参数是一个地址值,在这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。
规则二:延迟函数按后进先出(LIFO)的顺序执行,即先出现的defer最后执行
定义defer类似与入栈操作,执行defer类似于出栈操作。
设计defer的初衷是简化函数返回时清理资源的动作,资源往往有依赖关系,比如先申请A,再根据A资源申请B,再根据B资源申请C,即申请顺序A-->B-->C。释放资源时需要反向进行。这就是把defer设计成LIFO的原因。
每申请一个用完需要释放的资源时,立即定义一个defer来释放资源是一个好习惯。
func func5() {
for i := 0; i < 5; i++ {
defer fmt.Print(i)
}
}
输出:
43210
规则三:延迟函数可能操作主函数的具名返回值
当定义defer的函数(主函数)有返回值,返回值可能有名字(具名返回值),也可能没有名字(匿名返回值),延迟函数可能会影响返回值。
若要明白延迟函数是如何影响返回值的,只需要知道函数是如何返回的就够了
函数返回过程:关键字return不是一个原子操作,实际上return只代表汇编指令ret,即跳转程序执行。
比如 return i ,实际上分两步执行,即先将 i 值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是在跳转前,所以说defer执行时还是有机会操作返回值。
func deferFuncReturn() (result int) {
i := 1
defer func() {
result ++
}()
return i
}
该函数的return语句可以拆分成下面两行:
result = i
return
而延迟函数的执行正是在return之前,即加入defer后的执行过程如下:
result = i
result ++
return
defer使用场景:
defer语句用于延迟函数的调用,常用于关闭文件描述符、释放锁等资源释放场景。
1.释放资源(关闭文件句柄、数据库连接、停止定时器Tricker及关闭管道等资源清理资源)
m.mutex.Lock()
defer m.mutex.Unlock()
2.流程控制(控制函数执行顺序,配合wait.Group实现等待协程退出等)
var wg wait.Group
defer wg.Wait()
...
3.异常处理(与recover配合可以消除panic。且recover只能用于defer函数中)
defer func() { recover() } ()
defer面试题:
func func2() (val int) {
val = 10
defer func() {
fmt.Printf("defer里面 val:%d\n",val)
val += 2
}()
val++
return val
}
func printFunc2() {
fmt.Println(func2())
}
func main() {
prinFunc2()
}
输出:
defer里面 val:11
13
解释:主函数存在具名返回值val,执行步骤如下:
- 执行 val =10
- 创建defer(deferproc()负责把defer函数处理成_defer实例,并存入goroutine中的链表)
- 执行val++ (val=11)
- 执行return val ,可以分为两步,val = 11 和 return,但是return之前需要执行defer
- 执行defer(deferreturn()负责把defer从goroutine链表中的defer实例取出并执行)
- defer里面先打印 “defer里面 val:11”
- defer里面执行val += 2(val=13)
- 再执行return,跳转程序执行到printFunc2()
- 执行 printFunc2()里面的打印fun2返回的val(此时val=13)
func func3() (val int) {
val = 10
defer func() {
fmt.Printf("defer里面 val:%d\n",val)
val += 2
}()
val++
return 100
}
func printFunc3() {
fmt.Println(func3())
}
func main() {
prinFunc3()
}
输出:
defer里面 val:100
102
解释:具名返回值函数
func func4() (val int) {
val = 0
defer func() {
fmt.Printf("func3 before val += 5:%d\n",val)
}()
val+=5
defer func() {
fmt.Printf("func3 after val += 5:%d\n",val)
}()
return val
}
func printFunc4(){
fmt.Println(func4())
}
func main() {
prinFunc4()
}
输出:
func3 after val += 5:5
func3 before val += 5:5
5
解释:defer先入后出。
func foo() int {
var i int
defer func() {
i++
}()
return i
}
func main() {
fmt.Println(foo())
}
输出:
1
0
解释:上面的函数有匿名返回值,返回一个局部变量i的值,同时defer函数也会操作这个局部变量,对于匿名函数来说,可以假定仍然有一个变量存储返回值,假定返回值未anony,上面的返回语句可以拆分为以下过程:
anony = i
i++
return
由于i是整型值,会将值赋值给anony,所以在defer语句中修改i值,虽然i值改变了,但是返回的是anony,即不会对函数返回值造成影响。