Go中defer细节,避免掉坑

Posted by     "" on Wednesday, December 25, 2019

Go中defer细节,避免掉坑

我们先看下面这段代码, 通过代码去解读:

// func 1
func increaseA() int {
    var i int
    defer func() {
        i++
    }()
    return i
}

// func 2
func increaseB() (r int) {
    defer func() {
        r++
    }()
    return r
}

大部分人在没有运行测试的情况下完全答对的比较少,有部分人可能知道答案但是不明白其中的原因.所以有必须总结下.

什么是defer

defer 是Go语言提供的一种用于注册延迟调用的机制, 每一次defer都会把函数压入栈中, 当前函数返回前再把延迟函数取出并执行.

defer 语句并不会马上执行, 而是会进入一个栈, 函数return前, 会按先进后出(FILO)的顺序执行. 也就是说最先被定义的defer语句最后执行.先进后出的原因是后面定义的函数会依赖前面的资源, 自然要先执行;否则,如果前面先执行,那后面的函数的依赖就没有了.

采坑点

使用defer最容易采坑的地方是和带命名返回参数的函数一起使用时.

defer 语句定义时,对外部变量的引用是有两种方式的, 分别是作为函数参数和作为闭包引用.作为函数参数,则在defer定义时就把值传递给defer,并缓存起来;作为闭包引用的话,则会在defer函数真正调用时根据整个上下文确定当前的值.

要弄明白关键是要理解这条语句:
return xxx

这条语句并不是一个原子指令,经过编译后,变成了三条指令:

1. 返回值 = xxx
2. 调用 defer 函数
3. 空的return

1,3步才是return 语句真正的命令,第二步是defer定义的语句, 这里就有可能会操作返回值.

我们来做几个题目:

// 1
func f1() (r int) {
    defer func() {
        r++
    }()
    return 0
}
// 2
func f2() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t
}
// 3
func f3() (r int) {
    defer func(r int) {
        r = r + 5
    }()
    return 1
}

我们试着用上面的拆解方式,给出自己的答案:
第一题拆解过程:

func f1() (r int) {
    // 1. 赋值
    r = 0

    // 2.闭包引用, 返回值被修改
    defer func() {
        r++
    }()
    // 3.空的return
    return
}

defer是闭包引用,返回值被修改,所以f1()返回1

第二题拆解过程:

func f2() (r int) {
    t := 5
    // 1. 赋值
    r = t
    // 2.闭包引用,但是没有修改返回值`r`
    defer func() {
        t = t + 5
    }()
    // 3.空的return
    return
}

第二步没有涉及返回值r的操作,所以返回5

第三题拆解过程:

func f3() (r int) {
    // 1. 赋值
    r = 1

    // 2. r作为函数参数, 不会修改要返回的那个 r 值,defer里面的r是个副本
    defer func(r int) {
        r = r + 5
    }(r)
    // 3.空的return
    return
}

第二步,r函数的参数使用,是一份复制,defer 语句里面的r和外面的r其实是两个变量,里面变量的改变不会影响外层变量r,所以不是返回6,而是返回1

咱们现在回到开头的第二个例子中,increaseB()函数返回1, 分解如下:

func increaseB() (r int) {
    // 1. 赋值
    r = 0
    // 2.闭包引用, r++
    defer func() {
        r++
    }()
    // 3.空return
    return
}

那第一个例子怎么解呢?,我把代码再贴一遍:

func increaseA() int {
    var i int
    defer func() {
        i++
    }()
    return i
}

大家可能会注意到, 函数increaseA()是匿名返回值, 返回局部变量,同时defer函数也会操作这个局部变量.对于匿名返回值来说, 可以假定有一个变量存储返回值,比如假定返回值变量为anony,上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于i是整型, 会将值拷贝给anony,所有defer语句中修改i值,对函数返回值不造成影响,所以返回0.

看完这篇文章,大家应该对defer有了更深入的了解了吧.

「真诚赞赏,手留余香」

Richie Time

真诚赞赏,手留余香

使用微信扫描二维码完成支付