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函数--defer语句_第1张图片

 <说明> 可以看到,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因为没有注册,不会执行。

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 + 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

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

运行结果: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语言学习笔记,go,golang,defer语句)