该语句用于延迟调用指定的函数,它只能出现在函数或方法的内部,由defer 关键字以及针对某个函数的调用表达式组成。这里被调用的函数称为延迟函数。简单的示例如下
func outerFunc() {
defer fmt.Println("函数执行结束前一刻才会被打印")
fmt.Println("第一个被打印")
}
《代码说明》defer关键字后面是针对fmt.Println()函数的调用表达式。这里的outerFunc()称为外围函数。
defer语句经常用于处理成对的操作,如打开和关闭、连接和断开连接、加锁和释放锁等。通过defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放和回收。释放资源的defer应该直接跟在请求资源的语句后。
<注意> defer后面必须是函数或方法的调用,不能是普通语句,否则会报 "expression in defer must be function call" 错误。
正因为defer有这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。它有两个明显优势:
在Go语言中,return语句在底层并不是原子操作,它分为给返回值赋值和执行RET指令(汇编指令)两步。而defer语句执行时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
<说明> 可以看到,Go语言的return语句并不是RET汇编指令,它有两个步骤:1. 先更新返回值;2.再执行RET指令。
[参考] CALL和RET指令---汇编学习笔记
示例:defer经典案例。
func f1() int {
x := 5
defer 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 := 5
defer 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()) // 5
fmt.Println(f2()) // 6
fmt.Println(f3()) // 5
fmt.Println(f4()) // 5
fmt.Println(f5()) // 5
fmt.Println(f6()) // 6
}
defer 后面的延迟函数实参在注册时通过值拷贝传递进去,并且该函数在注册时所有的实参都需要确定其值。defer语句必须先注册后才能执行,如果defer位于return之后,则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
《代码说明》
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 + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer 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
处理业务或逻辑中涉及成对的操作是一件比较繁琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。
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语句来释放资源的写法更加简单和清晰,也更不容易被忽略。
相比直接用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
运行结果: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 的温柔陷阱