zoukankan      html  css  js  c++  java
  • Go defer 特性和使用场景

    golang 的 defer 语句用于延迟调用。defer 会在当前函数返回之前执行 defer 注册的函数。比如 defer func_defer() 这样语句会让你注册一个函数变量到 defer 的全局链表中,在 defer 语句所在的函数退出之前调用。

    defer 可以代替其它语言中 try…catch… 语句,也可以用来处理释放资源等收尾操作,比如关闭文件句柄、关闭数据库连接等。defer 还能用于 panic 的 recovery。

    1. defer 的特性

    我们先深入的剖析下 defer 具有的特性,知其然也。这些特性是需要我们记住的特点,才能更好的理解 defer 使用的场景。

    1) 延迟调用

    package main
    
    func main() {
        defer println("--- defer ---")
        println("--- end ---")
    }

    运行结果:

    --- end ---
    --- defer ---

    defer 会在 main 函数所有语句之后, return 之前时候调用。核心要点:

    • 延迟调用:defer 语句本身虽然是 main 的第一行,但是 println("--- end ---") 先打印的;
    • defer 关键字一定是处于函数上下文:defer 必须放在函数内部;

    2) LIFO

    一个函数中含有有多个 defer,调用顺序采用压栈式执行,后入先出(LIFO)。

    package main
    
    import (
        "strconv"
    )
    
    func main() {
        for i := 1; i <= 3; i++ {
            defer println("defer -->" + strconv.Itoa(i))
        }
        println("--- end ---")
    }

    压栈式执行,也就是说先注册的函数后调用。如上,我们注册的顺序式 1,2,3,最后打印 “--- end ---”,所以执行的结果自然是反着来的,程序输出:

    --- end ---
    defer -->3
    defer -->2
    defer -->1

    3) 作用域

    defer 只会和 defer 语句所在的特定函数绑定在一起,作用域也只在这个函数。 从语法上来讲,defer 语句也一定要在函数内,否则会报告语法错误。

    package main
    
    func main() {
        func() {
            defer println("--- defer ---")
        }()
        println("--- end ---")
    }

    如上,defer 处于一个匿名函数中,就 main 函数本身来讲,匿名函数 fun(){}() 先调用且返回,然后再调用 println("--- end ---") ,所以程序输出自然是:

    --- defer ---
    --- end ---

    4) 异常场景

    这个是非常重要的特性:panic 也能执行。golang 不鼓励异常的编程模式,但是却也留了 panic-recover 这个异常和捕捉异常的机制。所以 defer 机制就显得尤为重要,甚至可以说是必不可少的。因为你没有一个无视异常,永保调用的 defer 机制,很有可能就会发生各种资源泄露,死锁等场景。为什么?因为发生了 panic 却不代表进程一定会挂掉,很有可能被外层 recover 住。

    package main
    
    func main() {
        defer func() {
            if e := recover(); e != nil {
                println("--- defer ---")
            }
        }()
        panic("throw panic")
    }

    如上,main 函数注册一个 defer ,且稍后主动触发 panic,main 函数退出之际就会调用 defer 注册的匿名函数。再提一点,这里其实有两个要点:

    • defer 在 panic 异常场景也能确保调用;
    • recover 必须和 defer 结合才有意义;

     

    2. defer 使用场景

    1) 并发同步

    以下的例子对两个并发的协程做了下同步控制,常规操作。

    var wg sync.WaitGroup
    
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 程序逻辑
        }()
    }
    wg.Wait()

    2) 锁场景

    加锁解锁必须配套,在 golang 有了 defer 之后,你就可以写了 lock 之后,立马就写 unlock ,这样就永远不会忘了。

    mu.RLock()
    defer mu.RUnlock()

    但是请注意,lock 以下的代码都会在锁内。所以下面的代码要足够精简和快速才行,如果说下面的逻辑很复杂,那么可能就需要手动控制 unlock 防止的位置了。

    3) 资源释放

    某些资源是临时创建的,作用域只存在于现场函数中,用完之后需要销毁,这种场景也适用 defer 来释放。释放就在创建的下一行,这是个非常好的编程体验,这种编程方式能极大的避免资源泄漏。因为写了创建立马就可以写释放了,再也不会忘记了。

    // 创建一个客户端 client;
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    // 释放该 client ,也就是说该 client 的声明周期就只在该函数中;
    defer cli.Close()

    4) panic-recover

    recover 必须和 defer 结合才行,使用姿势一般如下:

    defer func() {
        if v := recover(); v != nil {
            _ = fmt.Errorf("PANIC=%v", v)
        }
    }()    

     

    3. 总结

    • defer 其实并不是 golang 独创,是多种高级语言的共同选择;
    • defer 最重要的一个特点就是无视异常可执行,这个是 golang 在提供了 panic-recover 机制之后必须做的补偿机制;
    • defer 的作用域存在于函数,defer 也只有和函数结合才有意义;
    • defer 允许你把配套的两个行为代码放在最近相邻的两行,比如创建&释放资源、加锁&释放锁等,使得代码更易读,编程体验优秀。

    参考资料

       1. go语言编程

     2. 编程宝库

  • 相关阅读:
    leetcode_24. 两两交换链表中的节点
    Mysql大数据量分页优化
    MySQL 默认排序是什么
    spring注入map,spring注入一个接口的多个实现类在map里
    eureka缓存细节以及生产环境的最佳配置
    MySQL 5.7性能调优
    安装后的十个MySQL性能调整设置(版本:5.1、5.5 和5.6)
    docker部署tomcat应用和MySQL数据库
    MySQL热备工具Xtrabackup
    MySQL数据库的冷备方式
  • 原文地址:https://www.cnblogs.com/wanghao72214/p/15556991.html
Copyright © 2011-2022 走看看