1 defer语句

该语句用于延迟调用指定的函数,它只能出现在函数或方法的内部,由defer 关键字以及针对某个函数的调用表达式组成。这里被调用的函数称为延迟函数。简单的示例如下

func outerFunc() {defer fmt.Println("函数执行结束前一刻才会被打印")fmt.Println("第一个被打印")
}

《代码说明》defer关键字后面是针对fmt.Println()函数的调用表达式。这里的outerFunc()称为外围函数

defer语句经常用于处理成对的操作,如打开和关闭、连接和断开连接、加锁和释放锁等。通过defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放和回收。释放资源的defer应该直接跟在请求资源的语句后。

<注意> defer后面必须是函数或方法的调用,不能是普通语句,否则会报 "expression in defer must be function call" 错误。

2 defer语句的特性

  • 当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数都执行完毕,外围函数才会真正结束执行。
  • 当执行外围函数的return语句时,只有其中所有的延迟函数都执行完毕后,外围函数才会真正返回函数返回值。
  • 当外围函数中的代码引发运行时恐慌时(即执行了panic语句),只有其中所有的延迟函数都执行完毕后,该运行时恐慌才会真正扩散至调用函数。

正因为defer有这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。它有两个明显优势:

  • 对延迟函数的调用总会在外围函数执行结束前执行。
  • defer语句在外围函数体中的的位置不限,并且数量不限。

3 defer执行时机

在Go语言中,return语句在底层并不是原子操作,它分为给返回值赋值和执行RET指令(汇编指令)两步。而defer语句执行时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:

<说明> 可以看到,Go语言的return语句并不是RET汇编指令,它有两个步骤:1. 先更新返回值;2.再执行RET指令。

[参考] CALL和RET指令---汇编学习笔记

示例:defer经典案例。

func f1() int {x := 5defer func() {x++   //修改的是变量x的值,不是返回值}()return x  //1.返回值赋值,即将x=5赋值给一个中间变量 2.执行defer语句 3.执行真正的RET指令,返回那个中间变量的值
}func f2() (x int) {defer func() {x++   //x是函数返回值}()return 5  //1.将常量5赋值给返回值变量x 2.执行defer语句,返回值x的变为6 3.执行真正的RET指令,返回函数返回值x的值
}func f3() (y int) {x := 5defer func() {x++   //x不是函数返回值,变量y才是函数返回值}()return x  //1.将x赋值给函数的返回值变量y=5, 2.执行defer语句,变量x的值变为6 3.执行真正的RET指令,返回函数返回值y的值
}func f4() (x int) {defer func(x int) {x++   //x是匿名函数的形参}(x)return 5  //1.将5赋值给函数返回值x 2.执行defer语句,改变的只是x的副本 3.执行真正的RET指令,返回函数返回值x的值
}func f5() (x int) {defer func(x int) int {x++       //改变的只是x的副本return x  //defer中的匿名函数的返回值没有使用到}(x)return 5
}//传一个x的指针到匿名函数中
func f6() (x int) {defer func(x *int) {(*x)++     //改变的是指针x指向的变量的值,亦即f6函数中的函数返回值变量x}(&x)return 5  //1.将返回值5赋值给函数返回值变量x 2.执行defer语句,返回值变量x的值变为6 3.执行真正的RET指令,返回函数返回值x的值
}func main() {fmt.Println(f1())  // 5fmt.Println(f2())  // 6fmt.Println(f3())  // 5fmt.Println(f4())  // 5fmt.Println(f5())  // 5fmt.Println(f6())  // 6
}

defer 后面的延迟函数实参在注册时通过值拷贝传递进去,并且该函数在注册时所有的实参都需要确定其值。defer语句必须先注册后才能执行,如果defer位于return之后,则defer因为没有注册,不会执行。

4 多个defer语句的执行顺序

在一个函数或者方法中,可以注册多个延迟调用,即有多个defer语句。这些defer语句的延迟函数的调用是按先进后出(FILO)的顺序在外围函数返回前被执行。

示例代码1:多个defer语句的执行顺序演示代码。

func main() {fmt.Println("start")//注册defer,将defer后面的延迟函数放入调用栈defer fmt.Println(1)defer fmt.Println(2)//最后一个defer,位于调用栈顶,最先调用defer fmt.Println(3)fmt.Println("end")
}

运行结果:

start
end
3
2
1

《代码说明》

  • 通过运行结果可以看到,延迟函数调用是在defer所在函数结束时进行,函数结束可以是正常返回(如return语句)时,也可以是发生宕机(如panic语句)时。
  • 当代码执行到 defer fmt.Println(1) 语句时,它会先注册要被调用的延迟函数,这个过程其实就是将延迟函数压入函数调用栈中。
  • 当函数流程执行完后,开始执行延迟函数,按FILO次序执行。这个也很好理解,因为这些延迟函数是存放在栈区结构中的。

5 defer语句的注意事项

1、如果在延迟函数中使用外部变量,应该通过参数传入。示例如下:

func printNumbers() {for i:=0; i<5; i++ {defer func(){fmt.Printf("%d", i)}()}
}// 代码分析如下:
// i=0, defer func()  注册匿名延迟函数1
// i=1, defer func()  注册匿名延迟函数2
// i=2, defer func()  注册匿名延迟函数3
// i=3, defer func()  注册匿名延迟函数4
// i=4, defer func()  注册匿名延迟函数5
// i=5, 开始逆序执行延迟函数,首先执行匿名延迟函数5,输出:5
// i=5, 执行匿名延迟函数4,输出:5
// i=5, 执行匿名延迟函数3,输出:5
// i=5, 执行匿名延迟函数2,输出:5
// i=5, 执行匿名延迟函数1,输出:5
// 因此,最终的输出结果为:55555

上述代码的执行结果为:55555。这正是延迟函数的执行时机引起的。等到开始执行那5个延迟函数时,它们使用的i值已经是5了。正确的做法是如下面这样:

func printNumbers() {for i:=0; i<5; i++ {defer func(n int){fmt.Printf("%d", n)}(i)}
}//代码分析如下:
// i=0, defer func(0) 注册匿名延迟函数1
// i=1, defer func(1) 注册匿名延迟函数2
// i=2, defer func(2) 注册匿名延迟函数3
// i=3, defer func(3) 注册匿名延迟函数4
// i=4, defer func(4) 注册匿名延迟函数5
// i=5, 开始逆序执行延迟函数,首先执行匿名延迟函数5 func(4) 输出:4
// i=5, 执行匿名延迟函数4 func(3) 输出:3
// i=5, 执行匿名延迟函数3 func(2) 输出:2
// i=5, 执行匿名延迟函数2 func(1) 输出:1
// i=5, 执行匿名延迟函数1 func(0) 输出:0
// 因此,最终的输出结果为:43210,而不是01234。

《代码说明》上面示例的输出结果为:43210,而不是01234。这与defer语句的执行顺序有关。这在上面的第4节中已有说明。还是再描述一下这个执行顺序的规则。

2、同一个外围函数内多个延迟函数调用的执行顺序,会与其所属的defer语句的执行顺序完全相反。同一个外围函数中每个defer语句在执行的时候,针对其延迟函数的调用表达式都会被压入同一个栈内。在外围函数执行结束前一刻,又会从调用栈中依次取出延迟函数并执行。

3、延迟函数调用若有参数传入,那么这些参数的值会在当前defer语句执行时求出以确定其值。请看下面的示例:

func printNumbers() {for i:=0; i<5; i++ {defer func(n int){fmt.Printf("%d", n)}(i * 2)}
}//代码分析如下:
// i=0, defer func(0 * 2) ==> defer func(0)
// i=1, defer func(1 * 2) ==> defer func(2)
// i=2, defer func(2 * 2) ==> defer func(4)
// i=3, defer func(3 * 2) ==> defer func(6)
// i=4, defer func(4 * 2) ==> defer func(8)
// i=5, 执行 func(8) 输出:8
// i=4, 执行 func(6) 输出:6
// i=5, 执行 func(4) 输出:4
// i=5, 执行 func(2) 输出:2
// i=5, 执行 func(0) 输出:0
// 因此,最终的输出结果为:86420

面试题:下面的代码输出结果是什么?

func calc(index string, a, b int) int {ret := a + bfmt.Println(index, a, b, ret)return ret
}func main() {x := 1y := 2defer calc("AA", x, calc("A", x, y))x = 10defer calc("BB", x, calc("B", x, y))y = 20
}//代码分析如下:
// 1. 给变量x,y赋值,x=1 y=2
// 2. defer calc("AA", 1, calc("A", 1, 2))
// 3. calc("A", 1, 2)  输出: A 1 2 3
// 4. defer calc("AA", 1, 3)  注册延迟函数1
// 5. x = 10
// 6. defer calc("BB", 10, calc("B", 10, 2))
// 7. calc("B", 10, 2)  输出: B 10 2 12
// 8. defer calc("BB", 10, 12)  注册延迟函数2
// 9. y = 20 至此,main()函数流程执行完毕,接下来开始执行延迟函数
// 10. calc("BB", 10, 12)  输出: BB 10 12 22
// 11. calc("AA", 1, 3)    输出: AA 1 3 4
// 12. main()函数真正地返回并结束整个程序的运行

运行结果:

A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

6 使用defer语句在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较繁琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

defer语句正好是在函数退出时执行的语句,所以使用defer能非常方便地处理资源释放的问题。使用defer的好处是可以在一定程度上避免资源泄漏的发生,特别是在有很多return语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。

示例1:打开和关闭文件的常规处理方式。代码如下:

func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return}dst, err := os.Create(dst)if err != nil {src.Close()  //src很容易忘记关闭!!!return}w, err := io.Copy(dst, src)//执行关闭文件操作dst.Close()src.Close()return
}

下面使用defer语句改写上面的代码,在打开资源无报错后直接调用defer语句关闭资源,一旦养成这样的编程习惯,就不会忘记资源的释放了。

func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return}defer src.Close()    //调用defer语句,将会在函数返回前被执行dst, err := os.Create(dst)if err != nil {return}defer dst.Close()    //调用defer语句,将会在函数返回前被执行w, err := io.Copy(dst, src)return
}

通过对比CopyFile()函数的两种不同写法,可以看出,使用defer语句来释放资源的写法更加简单和清晰,也更不容易被忽略。

7 defer语句性能分析

相比直接用CALL汇编指令调用函数,使用defer语句的延迟调用函数则需要较大代价。这其中包括延迟函数的注册、调用等操作,还有额外的内存开销。

以最常见的互斥锁mutex 为例,简单对比一下两者的性能差异。

var m sync.Mutex  //声明一个互斥锁变量func call(){m.Lock()m.Unlock()
}func deferCall(){m.Lock()defer m.Unlock()
}func Benchmark_call(b *testing.B) {for i:=0; i<b.N; i++ {call()}
}func Benchmark_deferCall(b *testing.B) {for i:=0; i<b.N; i++ {deferCall()}
}

运行结果:go test -v -bench=. benchmark_test.go

// go version go1.15.2 linux/amd64
goos: linux
goarch: amd64
Benchmark_call
Benchmark_call          72545788            17.9 ns/op
Benchmark_deferCall
Benchmark_deferCall     61465521            19.4 ns/op
PASS
ok      command-line-arguments  2.537s// go version go1.14.1 linux/amd64
goos: linux
goarch: amd64
Benchmark_call
Benchmark_call-4                82453729                14.5 ns/op
Benchmark_deferCall
Benchmark_deferCall-4           72586155                17.2 ns/op
PASS
ok      command-line-arguments  2.486s

《结果分析》从go1.14和go1.15两个版本的执行结果对比来看,defer语句每个op执行耗时虽然比直接用CALL指令要长一些,但是性能差距已经很小了,在Go的低版本的测试中,二者的性能可能相差数倍,看来Go语言开发者对defer语句的优化已经做得相当好了呀!

<建议> 对于那些性能要求高且压力大的算法,应尽量避免使用defer语句。

参考

《Go语言从入门到进阶实战(视频教学版)》

《Go语言学习笔记》

《Go并发编程实战(第2版)》

《Go语言核心编程》

Go语言基础之函数

Golang 之轻松化解 defer 的温柔陷阱

Go函数--defer语句相关推荐

  1. go语言的defer语句

    go语言defer语句的用法 参考:https://www.jianshu.com/p/5b0b36f398a2 defer的语法 defer后面必须是函数调用语句,不能是其他语句,否则编译器会出错. ...

  2. golang中defer语句使用小结

    defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源.关闭数据库连接.断开socket连接.解锁一个加锁的资源.Go语言机制担保一定会执行defer语句中的代 ...

  3. golang之defer语句

    文章目录 defer语句 释放资源 变量捕获 返回值影响 具名返回值 非具名返回值 defer语句会将其对应的函数延迟执行. defer语句 defer语句用于延迟函数调用,每次会把一个函数压入栈中, ...

  4. C++ Primer 5th笔记(chap 18 大型程序工具)函数 try 语句块与构造函数

    1. 问题 构造函数在进入函数体之前首先执行初始化列表.因为在初始值列表抛出异常时构造函数体内的try语句块还未生效 1.1 解决方法 {// 这是处理构造函数初始值错误的唯一方法template & ...

  5. mfc formview中的关闭视图函数_VC|API消息处理(回调函数+分支语句)与MFC中的消息映射函数...

    Windows程序不同于控制台程序,因为输入输出不再是scanf和printf那么简单了,而是通过窗口(包括对话框和控件)作为输入.输出的界面以及键盘.鼠标的各类输入事件. 用户在操作系统中的任何操作 ...

  6. SQL注入-常用函数和语句

    文章目录 前言 0x01 常用函数 0x02 常用语句 总结 前言 SQL的函数很多,这里简单介绍一下在SQL注入中比较常用到的一些函数,如果你SQL学的不是很好,又想练一练SQL手工注入的话,可以优 ...

  7. 【C语言】结构组成(函数、语句、注释)

    C语言结构组成 一.相关基础知识 二.具体内容 C语言由函数.语句和注释三部分组成: (1)函数与主函数:     一个C语言源程序可以由一个或多个源文件组成,每个源文件可由一个或多个函数组成,一个源 ...

  8. go defer 语句会延迟函数的执行直到上层函数返回。

    defer code... 可以理解为 执行完当前defer所在的方法代码后执行defer 中的代码 常用在释放资源 比如 关闭文件 为防止忘记编写关闭代码 可以先写好   defer  各种释放资源 ...

  9. python函数几个return语句_Python常用函数--return 语句-阿里云开发者社区

    在Python教程中return 语句是函数中常用的一个语句. return 语句用于从函数中返回,也就是中断函数.我们也可以选择在中断函数时从函数中返回一个值. 案例(保存为 function_re ...

最新文章

  1. 超棒整理 | Python 关键字知识点大放送
  2. Scala集合数据结构特点
  3. flash builder eclipse插件安装
  4. 虚拟资源拳王公社:什么都不会做什么副业赚钱?最容易上手的兼职副业是什么
  5. ggplot2图集汇总(一)
  6. 阶段3 1.Mybatis_08.动态SQL_03.mybatis中动态sql语句-foreach和sql标签
  7. cmos和ttl_TTL和CMOS的比较
  8. Oracle数据库实验报告六 PL/SQL基础
  9. APP兼容性覆盖测试
  10. VIVADO中使用BD时,常用的IP
  11. 使用Vendor NDK实现Android Camera preview
  12. uIP编译时配置选项
  13. mac自带邮箱添加邮箱_如何在Mac上的Mail中创建或删除邮箱
  14. 社团招新如何吸引新人,制作一张好的海报最关键
  15. 深度学习-深度学习集群管理方案
  16. Scrapy中的item和pipline
  17. 2021最新解除微信黑号方法
  18. Ubuntu22.04平台安装weston
  19. Mysql_基本操作命令
  20. 量化交易中,如何使用Python计算「筹码分布」指标【附代码】 [量化小讲堂-64]

热门文章

  1. PHP判断字符串中是否存在特殊符号,可判断中英文及特殊符号混合串
  2. 2840 WIKIOI——评测
  3. python pypy_Python之父的加速秘籍:PyPy能让代码运行得更快!
  4. java实现发送post请求
  5. 怪”博士闵万里:用人工智能,解决吃饭出行问题
  6. 【python】代码换行的几种方法
  7. 浅谈大数据如何存储?
  8. KiCAD批量修改丝印大小
  9. 自建防火墙日志分析系统V1
  10. 网站优化如何有效的堆砌关键词?