Go函数--defer语句
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语句相关推荐
- go语言的defer语句
go语言defer语句的用法 参考:https://www.jianshu.com/p/5b0b36f398a2 defer的语法 defer后面必须是函数调用语句,不能是其他语句,否则编译器会出错. ...
- golang中defer语句使用小结
defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源.关闭数据库连接.断开socket连接.解锁一个加锁的资源.Go语言机制担保一定会执行defer语句中的代 ...
- golang之defer语句
文章目录 defer语句 释放资源 变量捕获 返回值影响 具名返回值 非具名返回值 defer语句会将其对应的函数延迟执行. defer语句 defer语句用于延迟函数调用,每次会把一个函数压入栈中, ...
- C++ Primer 5th笔记(chap 18 大型程序工具)函数 try 语句块与构造函数
1. 问题 构造函数在进入函数体之前首先执行初始化列表.因为在初始值列表抛出异常时构造函数体内的try语句块还未生效 1.1 解决方法 {// 这是处理构造函数初始值错误的唯一方法template & ...
- mfc formview中的关闭视图函数_VC|API消息处理(回调函数+分支语句)与MFC中的消息映射函数...
Windows程序不同于控制台程序,因为输入输出不再是scanf和printf那么简单了,而是通过窗口(包括对话框和控件)作为输入.输出的界面以及键盘.鼠标的各类输入事件. 用户在操作系统中的任何操作 ...
- SQL注入-常用函数和语句
文章目录 前言 0x01 常用函数 0x02 常用语句 总结 前言 SQL的函数很多,这里简单介绍一下在SQL注入中比较常用到的一些函数,如果你SQL学的不是很好,又想练一练SQL手工注入的话,可以优 ...
- 【C语言】结构组成(函数、语句、注释)
C语言结构组成 一.相关基础知识 二.具体内容 C语言由函数.语句和注释三部分组成: (1)函数与主函数: 一个C语言源程序可以由一个或多个源文件组成,每个源文件可由一个或多个函数组成,一个源 ...
- go defer 语句会延迟函数的执行直到上层函数返回。
defer code... 可以理解为 执行完当前defer所在的方法代码后执行defer 中的代码 常用在释放资源 比如 关闭文件 为防止忘记编写关闭代码 可以先写好 defer 各种释放资源 ...
- python函数几个return语句_Python常用函数--return 语句-阿里云开发者社区
在Python教程中return 语句是函数中常用的一个语句. return 语句用于从函数中返回,也就是中断函数.我们也可以选择在中断函数时从函数中返回一个值. 案例(保存为 function_re ...
最新文章
- 超棒整理 | Python 关键字知识点大放送
- Scala集合数据结构特点
- flash builder eclipse插件安装
- 虚拟资源拳王公社:什么都不会做什么副业赚钱?最容易上手的兼职副业是什么
- ggplot2图集汇总(一)
- 阶段3 1.Mybatis_08.动态SQL_03.mybatis中动态sql语句-foreach和sql标签
- cmos和ttl_TTL和CMOS的比较
- Oracle数据库实验报告六 PL/SQL基础
- APP兼容性覆盖测试
- VIVADO中使用BD时,常用的IP
- 使用Vendor NDK实现Android Camera preview
- uIP编译时配置选项
- mac自带邮箱添加邮箱_如何在Mac上的Mail中创建或删除邮箱
- 社团招新如何吸引新人,制作一张好的海报最关键
- 深度学习-深度学习集群管理方案
- Scrapy中的item和pipline
- 2021最新解除微信黑号方法
- Ubuntu22.04平台安装weston
- Mysql_基本操作命令
- 量化交易中,如何使用Python计算「筹码分布」指标【附代码】 [量化小讲堂-64]