go 之 defer

example1

func f() (result int) { 
    defer func() { 
        result++ 
    }() 
    return 0
}

example2

func f() (r int) { 
    t := 5 
    defer func() { 
        t = t + 5 
    }() 
    return t
}

example3

func f() (r int) { 
    defer func(r int) { 
        r = r + 5 
    }(r) 
    return 1
}

先不要运行代码,自己在心里跑一遍结果。然后再去验证。如果三个都做对了并且不是蒙的…好吧,不用往下看了,你已经懂defer了。

多空几行确保你先在心里跑过一遍代码,之后验证了,并且存在疑惑…

额,如果example1中你算的是0,你就错了

如果example2中你觉得是10,你又错了…蒙对的不算…

如果example3中觉得得6,你又错了…如果你有算对的,也有算错,好吧…你丫的就是在蒙!

不懂的继续往下看啊…

首先要明确的是:defer是在return之前执行的

这是官方文档中明确说明的http://golang.org/ref/spec#Defer_statements ,知道就行了,可以无视

然后要了解是的defer的实现方式,defer出现的地方插入的指令

CALL runtime.deferproc

然后在函数返回之前的地方,插入指令

CALL runtime.deferreturn

再就是明确go返回值的方式跟C是不一样的,为了支持多值返回,go是用栈返回值的,而C是用寄存器。

最最最重要的一点就是:return xxx 这一句语句并不是一条原子指令!

整个return过程,没有defer之前是,先把在栈中写一个值,这个值被会当作返回值。然后再调用RET指令返回。return xxx语句汇编后是先给返回值赋值,再做一个空的return: ( 赋值指令 + RET指令)

defer的执行是被插入到return指令之前的

有了defer之后,就变成了 (赋值指令 + CALL defer指令 + RET指令)

而在CALL defer函数中,有可能将最终的返回值改写了…也有可能没改写。总之,如果改写了,那么看上去就像defer是在return xxx之后执行的~

这是所有你所想不明白的defer故事发生的根源。

上面的基础知识都有了,然后就可以来说说神奇的defer了。告诉大家一个简单的转换规则大家就再也不为defer迷糊了。

改写规则是将return语句分开成两句写,return xxx会被改写成:

返回值 = xxx

调用defer函数

空的return

先看example1。它可以改写成这样:


func f() (result int) { 

    result = 0 //return语句不是一条原子调用,return xxx其实是赋值+RET指令 

    func() { //defer被插入到return之前执行,也就是赋返回值和RET指令之间 

        result++ 

    }() 

    return

}

所以这个返回的是1

再看example2。它可以改写成这样:

func f() (r int) { 

    t := 5 

    r = t //赋值指令 

    func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 

        t = t + 5 

    } 

    return //空的return指令

}

所以这个的结果是5

最后看example3。它改写后变成:

func f() (r int) { 

    r = 1 //给返回值赋值 

    func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值 

        r = r + 5 

    }(r) 

    return //空的return

}

所以这个例子结果是1

懂了么?

结论:defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 RET之前,因此可能有机会改变最终的返回值。

当你觉得迷糊时,可以用我给的这套规则转一下代码。

golang中defer的使用规则
在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

为了更好的学习defer的行为,我们首先来看下面一段代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}

dst, err := os.Create(dstName)
if err != nil {
return
}

written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

这段代码可以运行,但存在’安全隐患’。如果调用dst, err := os.Create(dstName)失败,则函数会执行return退出运行。但之前创建的src(文件句柄)没有被释放。 上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。 如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。 而使用defer则可以避免这种情况的发生,下面是使用defer的代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
	return
	}
	defer src.Close()
	
	dst, err := os.Create(dstName)
	if err != nil {
	return
	}
	defer dst.Close()
	
	return io.Copy(dst, src)
}

通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。

规则一 当defer被声明时,其参数就会被实时解析

我们通过以下代码来解释这条规则:

	func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

读者自行编译看一下,结果输出的是0. why?

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。 换言之,上面的代码等同于下面的代码:

func a() {
	i := 0
	defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
	i++
	return
}

为了更为明确的说明这个问题,我们继续定义一个defer:

func a() {
	i := 0
	defer fmt.Println(i) //输出0,因为i此时就是0
	i++
	defer fmt.Println(i) //输出1,因为i此时就是1
	return
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出1,在输出0呢? 看下面的规则二。

规则二 defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。不要为什么,golang就是这么定义的。我们用下面的代码加深记忆和理解:

func b() {
	for i := 0; i < 4; i++ {
	defer fmt.Print(i)
	}
}

在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了3210.

规则三 defer可以读取有名返回值

先看下面的代码:

func c() (i int) {
	defer func() { i++ }()
	return 1
}

输出结果是2. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢…)。

当执行return 1 之后,i的值就是1. 此时此刻,defer代码块开始执行,对i进行自增操作。 因此输出2.

掌握了defer以上三条使用规则,那么当我们遇到defer代码块时,就可以明确得知defer的预期结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值