zoukankan      html  css  js  c++  java
  • 关于Go defer的详细使用

    先抛砖引玉defer的延迟调用:
    defer特性:

    1. 关键字 defer 用于注册延迟调用。
    2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
    3. 多个defer语句,按先进后出的方式执行。
    4. defer语句中的变量,在defer声明时就决定了。

    defer用途:

    1. 关闭文件句柄
    2. 锁资源释放
    3. 数据库连接释放

    好,废话不多说,实例加深理解,我们先看看一段代码

    package main
    
    import "fmt"
    
    func main() {
        var users [5]struct{}
        for i := range users {
            defer fmt.Println(i)
        }
    }

    输出:4 3 2 1 0 ,defer 是先进后出,这个输出没啥好说的。

    我们把上面的代码改下:
    defer 换上闭包

    package main
    
    import "fmt"
    
    func main() {
        var users [5]struct{}
        for i := range users {
            defer func() { fmt.Println(i) }()
        }
    }

    输出:4 4 4 4 4,很多人也包括我。预期的结果不是 4 3 2 1 0 吗?官网对defer 闭包的使用大致是这个意思:

    函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4。那么 如何正常输出预期的 4 3 2 1 0 呢?
    不用闭包,换成函数:

    package main
    
    import "fmt"
    
    func main() {
        var users [5]struct{}
        for i := range users {
            defer Print(i)
        }
    }
    func Print(i int) {
        fmt.Println(i)
    }

    函数正常延迟输出:4 3 2 1 0。

    我们再举一个可能一不小心会犯错的例子:
    defer调用引用结构体函数

    package main
    
    import "fmt"
    
    type Users struct {
        name string
    }
    
    func (t *Users) GetName() { // 注意这里是 * 传地址 引用Users
        fmt.Println(t.name)
    }
    func main() {
        list := []Users{{"乔峰"}, {"慕容复"}, {"清风扬"}}
        for _, t := range list {
            defer t.GetName()
        }
    }

    输出:清风扬 清风扬 清风扬。

    这个输出并不会像我们预计的输出:清风扬 慕容复 乔峰

    可是按照前面的go defer函数中的使用说明,应该输出清风扬 慕容复 乔峰才对啊?

    那我们换一种方式来调用一下

    package main
    
    import "fmt"
    
    type Users struct {
        name string
    }
    
    func (t *Users) GetName() { // 注意这里是 * 传地址 引用Users
        fmt.Println(t.name)
    }
    func GetName(t Users) { // 定义一个函数,名称自定义
        t.GetName() // 调用结构体USers的方法GetName
    }
    func main() {
        list := []Users{{"乔峰"}, {"慕容复"}, {"清风扬"}}
        for _, t := range list {
            defer GetName(t)
        }
    }

    输出:清风扬 慕容复 乔峰。

    这个时候输出的就是所谓"预期"滴了

    当然,如果你不想多写一个函数,也很简单,可以像下面这样(改2处),同样会输出清风扬 慕容复 乔峰

    package main
    
    import "fmt"
    
    type Users struct {
        name string
    }
    
    func (t *Users) GetName() { // 注意这里是 * 传地址 引用Users
        fmt.Println(t.name)
    }
    func GetName(t Users) { // 定义一个函数,名称自定义
        t.GetName() // 调用结构体USers的方法GetName
    }
    func main() {
        list := []Users{{"乔峰"}, {"慕容复"}, {"清风扬"}}
        for _, t := range list {
            t2 := t // 定义新变量t2 t赋值给t2
            defer t2.GetName()
        }
    }

    输出:清风扬 慕容复 乔峰。

    通过以上例子

    我们可以得出下面的结论:

    defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的*指针如何处理,

    通过这个例子可以看出go语言并没有把这个明确写出来的this指针(比如这里的* Users)当作参数来看待。到这里有滴朋友会说。看似多此一举的声明,

    直接去掉指针调用 t *Users改成 t Users 不就行了?

    package main
    
    import "fmt"
    
    type Users struct {
        name string
    }
    
    func (t Users) GetName() { // 注意这里是 * 传地址 引用Users
        fmt.Println(t.name)
    }
    
    func main() {
        list := []Users{{"乔峰"}, {"慕容复"}, {"清风扬"}}
        for _, t := range list {
            defer t.GetName()
        }
    }

    输出:清风扬 慕容复 乔峰。这就回归到上面的 defer 函数非引用调用的示例了。所以这里我们要注意defer后面的指针函数和普通函数的调用区别。很容易混淆出错。

    多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行,我们看看这一段

    package main
    
    func users(i int) {
        defer println("北丐")
        defer println("南帝")
    
        defer func() {
            println("西毒")
            println(10 / i) // 异常未被捕获,逐步往外传递,最终终止进程。
        }()
    
        defer println("东邪")
    }
    
    func main() {
        users(0)
        println("武林排行榜,这里不会被输出哦")
    }

    输出:

    东邪
    西毒
    南帝
    北丐
    panic: runtime error: integer divide by zero
    goroutine 1 [running]:
    main.users.func1(0x0)

    我们发现函数中异常,最后才捕获输出,但是一旦捕获了异常,后面就不会再执行了,即终止了程序。

    *延迟调用参数在求值或复制,指针或闭包会 "延迟" 读取。

    package main
    
    func test() {
        x, y := "乔峰", "慕容复"
    
        defer func(s string) {
            println("defer:", s, y) // y 闭包引用 输出延迟和的值,即y+= 后的值=慕容复第二
        }(x) // 匿名函数调用,传送参数x 被复制,注意这里的x 是 乔峰,而不是下面的 x+= 后的值
    
        x += "第一"
        y += "第二"
        println("x =", x, "y =", y)
    }
    
    func main() {
        test()
    }

    输出:

    x = 乔峰第一 
    y = 慕容复第二
    defer: 乔峰 慕容复第二

    defer 与 return注意

    package main
    
    import "fmt"
    
    func Users() (s string) {
    
        s = "乔峰"
        defer func() {
            fmt.Println("延迟执行后:"+s)
        }()
    
        return "清风扬"
    }
    
    func main() {
        Users() // 输出:延迟执行后:清风扬
    }

    解释:在有命名返回值的函数中(这里命名返回值为 s),执行 return "风清扬" 的时候实际上已经将s 的值重新赋值为 风清扬。

    所以defer 匿名函数 输出结果为 风清扬 而不是 乔峰。

    在错误的位置使用 defer,来一段不严谨滴代码:

    package main
    
    import "net/http"
    
    func request() error {
        res, err := http.Get("http://www.google.com") // 不翻墙的情况下。是无法访问滴
        defer res.Body.Close()
        if err != nil {
            return err
        }
    
        // ..继续业务code...
    
        return nil
    }
    
    func main() {
        request()
    }

    输出:

    panic: runtime error: invalid memory address or nil pointer dereference
    [signal 0xc0000005 code=0x0 addr=0x40 pc=0x5e553e]

    Why?因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,所以会抛出异常。

    怎么优化呢?

    我们应该总是在一次成功的资源分配下面使用 defer ,简单点说就是:当且仅当 http.Get 成功执行时才使用 defer.

    package main
    
    import "net/http"
    
    func request() error {
        res, err := http.Get("http://www.google.com")
        if res != nil {
            defer res.Body.Close()
        }
    
        if err != nil {
            return err
        }
    
        // ..继续业务code...
    
        return nil
    }
    
    func main() {
        request()
    }

    这样,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。

    解释:在这里,同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。

    通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,

    但其又会将错误返回。所以上面的代码保证了无论如何 Body 都会被关闭。

    另外我们再聊下关于文件的defer close。在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

    我们看一段代码:

    package main
    
    import "os"
    
    func open() error {
        f, err := os.Open("result.json") // 确保文件名存在
        if err != nil {
            return err
        }
    
        if f != nil {
            defer f.Close()
        }
    
        // ..code...
    
        return nil
    }
    
    func main() {
        open()
    }

    表面上看似没问题,其实f.Close可能关闭文件失败,我们优化下:

    package main
    
    import "os"
    
    func open() error {
        f, err := os.Open("result.json")
        if err != nil {
            return err
        }
    
        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    return
                }
            }()
        }
    
        // ..code...
    
        return nil
    }
    
    func main() {
        open()
    }

    如果有代码洁癖优化强迫症滴,哈哈。这里我们还可以优化下,可以通过命名的返回变量来返回 defer 内的错误。 如下:

    package main
    
    import "os"
    
    func open() (err error) {
        f, err := os.Open("result.json")
        if err != nil {
            return err
        }
    
        if f != nil {
            defer func() {
                if ferr := f.Close(); ferr != nil {
                    err = ferr //这里 通过命名的返回变量ferr赋值给err 来返回 defer 内的错误
                }
            }()
        }
    
        // ..code...
    
        return nil
    }
    
    func main() {
        open()
    }

    最后一个容易忽视的问题:如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行

    神马意思?继续看:

    package main
    
    import (
        "fmt"
        "os"
    )
    
    func open() error {
        f, err := os.Open("result.json")
        if err != nil {
            return err
        }
        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    fmt.Printf("延迟关闭文件result.json 错误 %v
    ", err)
                }
            }()
        }
    
        // ..code...
    
        f, err = os.Open("result2.json")
        if err != nil {
            return err
        }
        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    fmt.Printf("延迟关闭文件result2.json 错误 %v
    ", err)
                }
            }()
        }
    
        return nil
    }
    
    func main() {
        open()
    }

    输出:

    延迟关闭文件result.json 错误 close result2.json: file already closed

    结论:当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (result2.json)。

    而且两个 defer 都会将这个资源作为最后的资源来关闭,也就是优先关闭了result2.json后,再执行第一个defer Close result1.json的时候,

    其实还是在关闭result2.json.这样重复关闭同一个文件导致错误异常。肿么解决?很好办?用io.Closer属性

    package main
    
    import (
        "fmt"
        "io"
        "os"
    )
    
    func open() error {
        f, err := os.Open("result.json")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) { // 注意修改滴地方
                if err := f.Close(); err != nil {
                    fmt.Printf("延迟关闭文件result.json 错误 %v
    ", err)
                }
            }(f) // 注意修改滴地方
        }
    
        // ..code...
    
        f, err = os.Open("result2.json")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) {// 注意修改滴地方
                if err := f.Close(); err != nil {
                    fmt.Printf("延迟关闭文件result2.json 错误 %v
    ", err)
                }
            }(f)// 注意修改滴地方
        }
    
        return nil
    }
    
    func main() {
        open()
    }

    到此,关于Go中defer的使用总结到这里了,有更多的使用技巧或坑,欢迎诸位博友留言指正。。。。

  • 相关阅读:
    个人工作总结07
    软件项目第一个Sprint评分
    丹佛机场行李系统没能及时交工的原因
    第一次团队冲刺 5
    第一次团队冲刺4
    第一次团队冲刺3
    第一次团队冲刺2
    第一次团队冲刺 1
    风险评估
    团队开发——第一篇scrum报告
  • 原文地址:https://www.cnblogs.com/phpper/p/11984161.html
Copyright © 2011-2022 走看看