Go语言系列之defer

120 阅读6分钟

常记心中

理工科的学习一定要以基础知识为根本,基础知识又以概念的定义为基石。

基础知识里面的概念作为基石,都有其存在的必要性,所以当我们清楚了概念的同时,也要弄清楚其为什么存在。而为什么存在就是要搞清楚概念之间的联系。当探索的越来越多,我们可以说,在这个特定领域,最起码,我们站住脚了。继续前进吧~

defer的定义

A defer statement defers the execution of a function until the surrounding function returns.

一句话总结:defer就是用来延迟执行函数的技术手段。

虽然只有短短一句话,但是我们也可以获得很多。

来来来,我们先来总结一下,(更多的内容将在下文展开论述)。

1、defer后面跟函数:defers the execution of a function

2、defer的作用就是激活后接的函数:the execution.

3、运行时机:until the surrounding function returns.

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

为什么要有defer

defer的本质就是延迟自动执行函数

它的出现就是为了更加便利的进行资源管理,减轻程序员的心智负担,提高代码的可读性、可维护性和可扩展性。

假如go没有defer机制,为了实现一个受保护的文件操作。我们得这么写

func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
    mu.Lock()
    f, err := os.OpenFile(fname, os.O_RDWR, 0666)
    if err != nil {
    mu.Unlock()
    return err
    }

    _, err = f.Seek(0, 2)

    if err != nil {
        f.Close()
        mu.Unlock()
        return err
    }

    _, err = f.Write(data)
    if err != nil {
        f.Close()
        mu.Unlock()
        return err
    }

    err = f.Sync()
    if err != nil {
        f.Close()
        mu.Unlock()
        return err
    }

    err = f.Close()
        if err != nil {
        mu.Unlock()
        return err
    }

    mu.Unlock()
    return nil
}

就像老奶奶的裹脚布——又臭又长。程序员每次操作,都得处理资源的释放:锁的释放、文件句柄的关闭。仅仅两个资源的释放操作,就得『如临深渊,如履薄冰』。增加资源的参与数,那简直不堪设想。

幸好,我们的Godefer机制,我们可以改写如下

func writeToFile(fname string, data []byte, mu *sync.Mutex) error {

    mu.Lock()
    defer mu.Unlock()

    f, err := os.OpenFile(fname, os.O_RDWR, 0666)
    if err != nil {
        return err
    }
    defer f.Close()
    
    _, err = f.Seek(0, 2)
    if err != nil {
        return err
    }

    _, err = f.Write(data)
    if err != nil {
        return err
    }

    return f.Sync()
}

我们所需要做的就是申请资源之后,马上defer+释放资源的逻辑。当writeToFile运行结束时,会自动释放锁和关闭文件句柄。

让我们编写需要释放资源的代码时非常的便利!

defer的特性

上面对defer的描述,只是非常粗糙的讲解。下面讲述为了释放资源,延迟执行,defer的具体规定细节。

比如

defer后接函数,那么函数的参数什么时候确定?(要知道同样的变量,在函数退出时与刚进入函数时,内容可能是不一样的)可以后接方法吗?

defer后的函数真正执行时机是:声明defer表达式的函数执行结束时。那么这个所谓的函数执行结束时,是返回后,还是返回前?还是其他什么时间点?

如果一个函数之中,声明了多个defer表达式,虽然都是函数退出时调用,但是它们执行的顺序如何?随机的?(像select channel一样)?先进先出?(越先被defer的函数越早调用)?先进后出?

来吧,让我们回答上面的问题。

不过在此之前,学习两个英文单词。

execution:中文是执行的意思。比如函数的执行,就是运行函数的意思。

evaluate:中文是评价、估计的意思。在编程里面,比如函数参数,当我们说evaluate function parameters,就是确定函数参数值的意思。嗯,evaluate可以翻译成确定。

下面进入正题

1、defered function evaluates its parameter when is occur

defer的函数会立马确定后接函数参数

func print(num int) {
    fmt.Println(num)
}

func foo() {
    var i = 666
    defer print(i)
    
    i = 888
    return
}

func main() {

    foo()
}

会打印666。这就叫做当出现defer时,后接函数的参数立马被确定,即i=666。虽然defer在后面才会执行,但是参数的确定是在一开始就确定了。

2、defered function will execute before real return

defer的函数会在调用它的函数返回前执行

这句话不好理解。重点在于什么叫做real return

休息一下,下面的例子需要仔细看

func sara() (result int) {

    result = 1

    defer func() {
        result = 666
    }()

    return result

}

func lisa() int {

    var result = 1
    
    defer func() {
        result = 666
    }()

    return result
}

func main() {

    r := sara()
    fmt.Printf("sara finially result, %d\n", r) // 666

    r = lisa()
    fmt.Printf("lisa finially result, %d\n", r) // 1
}

可以观察到,saralisa基本逻辑是一样的,唯一的不同就是sara是具名返回值函数,而lisa是匿名返回值函数。这就是大大的不同了。

好好理解一下上面的例子,下面给出go函数的返回值模型

其实goreturn语句是分成两部分的。比如对于匿名返回值函数lisa来说

return result
相当于

1var ret = result // ret 是真返回值
2return ret

对于具名返回值函数sara来说

return result
相当于

1var result = result
2return result

注意两者的区别。

重点来啦,defer的运行时机就是步骤1和步骤2之间!

所以对于lisa来说,我要返回的是ret,就算你在defer中修改了result,也不会影响到最终返回值哦。但是sara就不一样了,她返回的就是result,在defer中修改了result,最终的结果就不一样啦。

咳咳,具名返回值和defer的联合使用,要注意哦😑。别写这样的代码。

3、多个defer的执行的顺序是:后进先出,类似栈

func print(num int) {
    fmt.Println(num)
}

func bob() {
    defer print(1)
    defer print(2)
    defer print(3)

    return

}

func main() {
    bob()
}

会打印

3
2
1

如何记住defer的特性

defer出现就是为了资源的管理。

比如如果有以下这个过程:首先获取资源A,然后获取资源B,最后获取资源C才能执行某个操作,那么defer的顺序,肯定是先放回C资源,然后B资源,最后是A资源。

这个过程让我想起来的举证的乘法和逆运算——逆矩阵。

比如AB,表示矩阵A乘以矩阵B,那么它俩的逆运算呢?

(AB)1=B1A1(AB)^{-1} = B^{-1}A^{-1}

当时为了记住这个特性,我是记住了下面的这个类比:

先穿袜子,然后穿鞋;如果这一过程反过来,那就是先脱鞋,再脱袜子。

nice

如今看来,不论是穿衣服、矩阵乘法、资源的管理,都是一种行为、一种运动、一种变化。他们是分先后的,所以他们的“反运动”要与正向运动相反。

真实世界中的使用案例

这个还真没遇到特别精彩的使用。都是中规中矩的释放资源,在上面已经有了多个例子。

姑且等待补充吧~

参考

  • go官方文档和教程

  • <100 Go mistakes>

  • <The Golang Programming Language>

  • 白明《Go语言精进之路》