• 理解Golang中defer的使用


    在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。
    通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。

    defer的实现

    defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。

    在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

    普通的函数返回时,汇编代码类似:

    add xx SP
    return

     

    如果其中包含了defer语句,则汇编代码是:

    call runtime.deferreturn,
    add xx SP
    return

    goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

    goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

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

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

    package main
    import "fmt"
     
     func main() {
    i := 0
    defer fmt.Println(i)
    i++
    return
     }
    ./hello 
    0

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

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

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

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

    defer调用顺序

    ./hello 
    1
    0

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

    再看一个例子:

    defer  和匿名函数

    package main
    import "fmt"
    func f1() (result int) {
        fmt.Println("enter f1")
        defer func() {
        fmt.Println("defer f1")
            result++  //操作返回值
        }()
        return 0
    }
    
    func f2() (r int) {
        fmt.Println("enter f2")
        t := 5
        defer func() {
        fmt.Println("defer f2")
            t = t+5
        }()
        return t   //返回10吗
    }
    
    func f3() (t int) {
        fmt.Println("enter f3")
        t = 5  
        defer func() {
        fmt.Println("defer f3")
            t = t+5  // 操作外围返回值
        }()
        return t  // 返回10吗
    }
    func f4() (r int) {
        fmt.Println("enter f4")
        defer func(r int) {
        fmt.Println("defer f4")
            r = r + 5
        }(r)
        return 1
    }
    
    func main() {
        fmt.Println(f1())
        fmt.Println(f2())
        fmt.Println(f3())
        fmt.Println(f4())
    }

    root@ubuntu:~/go_learn/example.com/hello# ./hello
    enter f1
    defer f1
    1
    enter f2
    defer f2
    -------------不是10 
    enter f3
    defer f3
    10
    enter f4
    defer f4
    1

    函数返回的过程是这样子的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

    defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致

    可以将return xxx改成
    返回值=xxx
    调用defer函数
    空的return
     

    那上面的例子就可以改成:

    func f11() (result int) {
        result = 0   //先给返回值赋值
        func(){               //再执行defer 函数
            result++
        }()
        return                //最后返回
    }
    
    func f22() (r int) {
        t := 5
        r = t //赋值指令
        func() {  //defer 函数被插入到赋值与返回之间执行,这个例子中返回值r没有被修改
            t = t+5
        }
        return   //返回
    }
    
    func f33() (t int) {
        t = 5    //赋值指令
        func(){
            t = t+5  //然后执行defer函数,t值被修改
        }
        return
    }
    func f44() (r int) {
        r = 1    //给返回值赋值
        func(r int){   //这里的r传值进去的,是原来r的copy,不会改变要返回的那个r值
            r = r+5
        }(r)
        return
    }
     

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

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

    package main
    import "fmt"
     
    
    func main() {
    for i := 0; i < 4; i++ {
    defer fmt.Println(i)
    }
    }
    ./hello 
    3
    2
    1
    0

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

    再看之前的那个例子:

    package main
     
    import "fmt"
     
    func main() {
        fmt.Println("a return:", a()) // 打印结果为 a return: 0
    }
     
    func a() int {
        var i int
     
        defer func() {
            i++
            fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
        }()
     
        defer func() {
            i++
            fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
        }()
     
        return i
    }
    ./hello
    a defer1: 1
    a defer2: 2
    a return: 0

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

    先看下面的代码:

    package main
     
    import "fmt"
    func a() (i int) {
        defer func() { i++ }()
        return 1
    }
    func main() {
        fmt.Println("a return:", a())  
    }
    ./hello
    a return: 2

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

    package main
    
    import "fmt"
    func trace(s string) string {
        fmt.Println("entering:",s)
        return s
    }
    
    func un(s string) {
        fmt.Println("leaving:",s)
    }
    
    func a() {
        defer un(trace("a"))
        fmt.Println("in a")
    }
    
    func b() {
        defer un(trace("b"))
        fmt.Println("in b")
        a()
    }
    
    func main() {
        b()
    }
    entering: b
    in b
    entering: a
    in a
    leaving: a
    leaving: b
  • 相关阅读:
    Linq to xml 示例分析
    Nhibernate学习笔记
    SQL – 11.练习
    集合 ArrayList
    分享一个不错的VS插件——CodeMap(转发)
    wamp配置虚拟主机,虚拟目录,伪静态
    RBAC (RoleBased Access Control)基于角色的访问控制
    PHP的面向对象
    一名靠谱的JavaScript程序员应备的素质
    Javascript 广告定位
  • 原文地址:https://www.cnblogs.com/dream397/p/15036004.html
Copyright © 2020-2023  润新知