1、defer的作用
defer后面的函数在defer语句所在的函数执行结束的时候会被调用,用来做一些收尾工作,当然它比面向对象语言中的析构函数
强大,因为它还有其他的任务,比如,异常捕获,资源释放,修改函数返回值。
defer在实际应用中非常常见,因此需要对其有一个透彻的理解。
defer后面必须是函数调用语句,不能是其他语句,否则编译器会出错。
2、defer的三条行为规则
2.1 规则一:延迟函数的参数在defer语句出现时就已经确定下来了
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
defer语句中的fmt.Println()
的参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。
后面对变量i的修改不会影响fmt.Println()
函数的执行,仍然打印 0。
再比如:
func tt2() {
a := []int{1, 2}
defer func(x []int){
fmt.Println(x) // [1 2] 复制了切片
}(a)
a = append(a, 3, 4, 5)
}
对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,如果指针指向的变量值发生了变化,那么会影响到延迟函数的指向结果。
func tt3() {
a := &Person{"Rao", 12}
defer func(p *Person){
fmt.Println(p.Name) // xiao
}(a)
a.Name = "xiao"
}
由以上例子可知,defer语句在定义的时候会复制实参的值到临时变量,临时变量的值就此固定下来,要么是值要么是指针。
注意这里说的是延迟函数的参数,比如:
defer func(x int) {
fmt.Println(x)
}(i)
如果延迟函数不需要传参呢:
defer func() {
fmt.Println(i)
}()
此时的变量 i 是没有固定下来的。
如果延迟函数的参数是另一个函数 ff 的返回值,那么在定义 defer 语句的时候,ff 函数会被执行,以拿到返回值,确定延迟函数的额参数。
2.2 规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
这个规则很好理解,定义defer类似于入栈操作,执行defer类似于出栈操作。
设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再跟据A资源申请B资源,跟据B资源申请C资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把deffer设计成FIFO的原因。
每申请到一个用完需要释放的资源时,立即定义一个defer来释放资源是个很好的习惯。
2.3 规则三:延迟函数可能操作主函数的具名返回值
定义defer的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer所作用的函数,即延迟函数可能会影响到返回值。
若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。
有一个事实必须要了解,关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。
比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
举个实际的例子进行说明这个过程:
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
返回值为 2
return语句执行过程:
ret指令将 i 赋值给 result;
然后执行 defer 函数;
result++
函数跳转
再比如:
func tt5() int {
i := 1
defer func() {
i++
}()
return i
}
返回值为 1
return语句执行过程:
将 i 赋值给 临时变量temp;
然后执行 defer 函数;
i++
关于主函数有不同的返回方式,但返回机制就如上面介绍所说,只要把return语句拆开都可以很好的理解。
同样的,对于指针变量,也可以这样分析。
func tt6() *Person {
a := &Person{"Rao", 12}
defer func(p *Person){
p.Name = "xiao"
}(a)
return a
}
返回值 &{xiao 12}
3、defer使用注意
defer后面的函数在defer语句所在的 函数执行结束
并且 函数返回前
的时候会被调用。
设想有一个需要长期运行的函数,其中有无限循环语句,在循环体内不断的创建资源(或分配内存),并用defer语句确保释放。由于函数一直运行没有返回,所有defer语句都得不到执行,循环过程中创建的大量短暂性资源一直积累着,得不到回收。而且,系统为了存储defer列表还要额外占用资源,也是持续增加的。这样下去,过不了多久,整个系统就要因为资源耗尽而崩溃。
例如:
func tt7() {
for {
f, err := os.Open(...)
defer f.Close()
...
}
}
由于 tt7 函数永远不会返回,所以 defer 语句永远不会被执行到。
针对这种情况,需要做一点优化,使系统资源得到释放,在defer外层套一个闭包函数就可以了,如下:
func tt7() {
for {
func(){
f, err := os.Open(...)
defer f.Close()
...
}()
}
}
4、defer 与 panic
一旦 defer 注册成功,那么后面即使有 panic 也不会影响其执行。
func main() {
re := ttss()
fmt.Println(re)
}
func ttss() (result bool) {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
result = tts()
return result
}
func tts() (result bool) {
panic("xxx")
return true
}
打印
xxx
false