Go语言学习——深入了解defer

tianqy · 2020-03-29 15:14:26 · 5177 次点击    
这是一个分享于 2020-03-29 15:14:26 的资源,其中的信息可能已经有所发展或是发生改变。

1.简介
defer是Go语言中的延迟调用函数,它是在函数真正返回之前立即执行,一般用于资源回收、捕捉异常操作,其实现原理是:
1)在对应语句打上标签,告诉系统,先不要调用、先执行入栈操作,待函数退栈时在一并调用;
2)当defer语句入栈时,会将函数地址、函数实参一同入栈,如果实参是表达式、函数语句,则会优先运行;
3)函数退栈时,无论是正常调用结束还是触发panic,都会调用之前入栈的defer语句,若有多个defer语句时,则按照FILO顺序执行调用;

2.使用
下面介绍defer语句的使用特点。

2.1延迟调用
defer是延迟调用的,是在函数退栈前才被调用的,如:

func main() {                                                        
    defer fmt.Println("A")                                                    
    fmt.Println("B")                                                    
}

输出顺序:B A

2.2生效时机
defer语句只有在被声明、入栈的前提下,才会有效,否则,函数退栈时不会被调用,如:

func main() {                                                        
    defer fmt.Println("A")                                                    
    return                                                    
    defer fmt.Println("B")            // 由于提前return,此处,defer语句失效                                        
}

输出顺序:A

2.3后于return语句
defer语句是延迟调用的,但和return语句细比较,return语句先执行,defer语句后执行,如:

func NewTest() int {                                                        
    fmt.Println("B")                                                    
    return 0                                                    
}                                                        

func Test() int {                                                        
    defer fmt.Println("A")                                                    
    return NewTest()            // 先执行该语句                                        
}                                                        

func main() {                                                        
    Test()                                                    
}

输出顺序:B A
需要注意的是,此处先执行的是return语句,但还没进入函数退栈、修改寄存器的处理步骤。

2.4执行顺序
多个defer语句的执行顺序是FILO,如:

func main() {                                                        
    defer fmt.Println("A")                                                    
    defer fmt.Println("B")                                                    
    fmt.Println("C")                                                    
}                                                        
输出顺序:C B A                                                        

func test() {                                                        
    for i := 0; i < 4; i++ {                                                    
        defer fmt.Print(i)                                                
    }                                                    
}

2.5嵌套
当defer发生嵌套时,内层的defer也是按照FILO的顺序执行,并且不影响到外层执行顺序,如:

func main() {                                                        
    defer fmt.Println("A")                                                    

    defer func() {                // defer整体执行顺序保持不变,依据是:                                    
        defer fmt.Println("B")                // fmt.Println("E")                                
        fmt.Println("C")                // func() {}()                                
        defer fmt.Println("D")                // fmt.Println("A")                                
    }()                // 而// func() {}()内部会继续按照FILO的顺序执行                                    

    defer fmt.Println("E")                                                    
}

输出顺序:E C D B A

2.6传参
之前举例中,defer语句中要么没有实参,要么实参取值明确,但在实际调用中,参数传递情况是未知的,如果传的是变量、表达式、函数语句,则会计算出实参结果,在随defer语句入栈。

2.6.1普通变量
defer语句传参中,实参是普通变量,此时,普通变量的取值,就会被压入栈中,如:

func main() {                        
    for index := 0; index < 5; index++{                                            
        defer fmt.Printf("%d ", index)                                            
    }                                                
}

输出顺序:4 3 2 1 0

2.6.2指针变量
defer语句传参中,实参是指针变量,此时,指针变量的地址会被压入栈中,而入栈时变量的取值是啥,已经不关注了,关注的是出栈执行时,指针变量的取值,如:

func main() {                                                        
    for index := 0; index < 5; index++{                                                    
        defer func(index *int){fmt.Printf("%d ", *index)}(&index)                                                
    }                                                    
}

输出顺序:5 5 5 5 5

2.6.3函数语句
defer语句传参中,实参是函数语句,此时,会先执行函数,并将函数的返回值压入栈中,如:

func main() {                                                        
    f := func(num int)int{                                                    
        fmt.Printf("%d ", num)                                                
        return 10 * num                                                
    }                                                    

    for index := 0; index < 5; index++{                                                    
        defer fmt.Printf("%d ", f(index))            // 第一循环时,先执行函数f(0),然后,将函数返回值入栈                                    
    }                                                    
    fmt.Println("")                                                    
}

输出顺序:0 1 2 3 4 40 30 20 10 0

2.7函数返回值
defer语句会影响到函数返回值的结果,此处要先讲一个知识点,对于有返回的函数,return语句执行的处理就是给返回值赋值,如:

func Test() (val int) {                                                        
    return 2                    // 此处执行的处理是 val = 2                                    
}

2.7.1defer语句操作返回值
defer语句操作函数返回值,此时,返回值的取值会被修改,如:

func Test(val int) (rst int) {                                                        
    defer func() {                                                    
        rst += val            // 在执行defer语句,对 rst 执行加运算,即 rst = 20 + 10,最终返回值rst取值为30                                    
    }()                                                    

    return 20                // 先执行return语句,对返回值变量rst执行赋值操作,即 rst = 20                                    
}                                                        

func main() {                                                        
    fmt.Println(Test(10))                                                    
}

输出顺序:30

再举个例子,修改返回值是指修改函数语句中的返回值,而不是return语句中的变量,如:

func Test(val int) int {        // 返回是int类型,系统会先生成一个临时变量,假设名字是 tmp                                    
    rst := val                                                    
    defer func() {                                                    
        rst += 20            // 在执行defer语句,对 rst 执行加运算,即 rst = 20 + 10,最终返回值rst取值为30,但tmp还是10                                    
    }()                                                    

    return rst                // 执行return语句,即 tmp = rst、 tmp = 10                                    
}                                                        

func main() {                                                        
    fmt.Println(Test(10))                                                    
}

输出顺序:10

2.8配合使用
2.8.1闭包
该种情况是指defer语句的函数体用到外面变量的情况,类似闭包调用,此时,入栈的只有函数地址,不涉及到变量,所以出栈执行时,变量取的是出栈时的值,如:

func main() {                                                        
    for index := 0; index < 5; index++{                                                    
        defer func() {fmt.Printf("%d ", index)}()                                                
    }                                                    
}

输出顺序:5 5 5 5 5

2.8.2panic
defer常用于异常捕捉,与panic、recover配合使用,处理规则是:defer先与panic声明,当有多个panic时,只有最后一个panic生效,并且recover也捕捉最后一个panic,如:

func main()  {                                                        
    defer func() {                                                    
        if err := recover(); err != nil{                                                
            fmt.Println(err)                                            
        }else {                                                
            fmt.Println("fatal")                                            
        }                                                
    }()                                                    

    defer func() {                                                    
        panic("defer panic")                                                
    }()                                                    

    panic("main panic")                                                    
}

输出顺序是:defer panic

3.注意
1)defer延迟调用的方式导致其存在浪费内存空间的问题,因为系统需要将defer延迟调用的内容先存下来、然后等函数退栈时再调用,所以defer也要按需使用;


入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

5177 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传