golang中的defer语句你真的会吗?

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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值