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

tianqy · · 747 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

1.简介 defer是Go语言中的延迟调用函数,它是在函数真正返回之前立即执行,一般用于资源回收、捕捉异常操作,其实现原理是: 1)在对应语句打上标签,告诉系统,先不要调用、先执行入栈操作,待函数退栈时在一并调用; 2)当defer语句入栈时,会将函数地址、函数实参一同入栈,如果实参是表达式、函数语句,则会优先运行; 3)函数退栈时,无论是正常调用结束还是触发panic,都会调用之前入栈的defer语句,若有多个defer语句时,则按照FILO顺序执行调用; 2.使用 下面介绍defer语句的使用特点。 2.1延迟调用 defer是延迟调用的,是在函数退栈前才被调用的,如: ```go func main() { defer fmt.Println("A") fmt.Println("B") }``` 输出顺序:B A 2.2生效时机 defer语句只有在被声明、入栈的前提下,才会有效,否则,函数退栈时不会被调用,如: ```go func main() { defer fmt.Println("A") return defer fmt.Println("B") // 由于提前return,此处,defer语句失效 }``` 输出顺序:A 2.3后于return语句 defer语句是延迟调用的,但和return语句细比较,return语句先执行,defer语句后执行,如: ```go 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,如: ```go 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的顺序执行,并且不影响到外层执行顺序,如: ```go 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语句传参中,实参是普通变量,此时,普通变量的取值,就会被压入栈中,如: ```go func main() { for index := 0; index < 5; index++{ defer fmt.Printf("%d ", index) } }``` 输出顺序:4 3 2 1 0 2.6.2指针变量 defer语句传参中,实参是指针变量,此时,指针变量的地址会被压入栈中,而入栈时变量的取值是啥,已经不关注了,关注的是出栈执行时,指针变量的取值,如: ```go 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语句传参中,实参是函数语句,此时,会先执行函数,并将函数的返回值压入栈中,如: ```go 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语句执行的处理就是给返回值赋值,如: ```go func Test() (val int) { return 2 // 此处执行的处理是 val = 2 }``` 2.7.1defer语句操作返回值 defer语句操作函数返回值,此时,返回值的取值会被修改,如: ```go 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语句中的变量,如: ```go 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语句的函数体用到外面变量的情况,类似闭包调用,此时,入栈的只有函数地址,不涉及到变量,所以出栈执行时,变量取的是出栈时的值,如: ```go 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,如: ```go 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

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