defer是什么?

在Go语言中,可以使用关键字defer向函数注册退出调用,即主函数退出时,defer后的函数才被调用
defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("test 1111")
  5. defer fmt.Println("test defer 1111")
  6. defer fmt.Println("test defer 2222")
  7. a := 10
  8. // defer a++ 编译报错 defer 后面只能跟函数调用
  9. fmt.Println("test 2222")
  10. /**
  11. test 1111
  12. test 2222
  13. test defer 2222
  14. test defer 1111
  15. */
  16. }

defer定义顺序与实际执行顺序相反

  1. package main
  2. import "fmt"
  3. /**
  4. 栈 先进后出
  5. cccccccccccccc
  6. bbbbbbbbbbbbbb
  7. aaaaaaaaaaaaaa
  8. */
  9. func main() {
  10. defer fmt.Println("aaaaaaaaaaaaaa")
  11. defer fmt.Println("bbbbbbbbbbbbbb")
  12. defer fmt.Println("cccccccccccccc")
  13. /**
  14. cccccccccccccc
  15. bbbbbbbbbbbbbb
  16. aaaaaaaaaaaaaa
  17. */
  18. }

defer执行时的拷贝机制

  1. package main
  2. import "fmt"
  3. func defer2() {
  4. x := 10
  5. // 压栈 压栈时参数进行了值拷贝,不受x++影响
  6. defer func(a int) {
  7. fmt.Println(a) // 10
  8. }(x)
  9. x++ // x 压栈
  10. }
  11. func defer3() {
  12. x := 10
  13. // 进行了压栈,但并不是传参
  14. defer func() {
  15. // 闭包的概念
  16. fmt.Println(x) // 11
  17. }()
  18. x++
  19. }
  20. func defer4() {
  21. x := 10
  22. // 进行了压栈,引用传递 指针地址
  23. defer func(x *int) {
  24. // 闭包的概念
  25. fmt.Println(*x) // 11
  26. }(&x)
  27. x++
  28. }
  29. func main() {
  30. defer2()
  31. defer3();
  32. defer4();
  33. }

defer 注意要点

  1. // 函数返回过程
  2. func deferFuncReturn() (result int) {
  3. i := 1
  4. defer func() {
  5. result++
  6. }()
  7. return i
  8. /**
  9. 延迟函数的执行正是在return之前,即加入defer后的执行过程如下:
  10. result = i
  11. result++
  12. return
  13. 所以上面函数实际返回i++值 为2
  14. */
  15. }
  16. // 主函数拥有匿名返回值,返回字面量
  17. func foo() int {
  18. var i int
  19. defer func() {
  20. i++
  21. }()
  22. return 1
  23. /**
  24. 返回一个局部变量,同时defer函数也会操作这个局部变量。
  25. 对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为“anony”,上面的返回语句可以拆分成一下过程:
  26. anony=i
  27. i++
  28. return
  29. 由于i是整形,会将值拷贝给anony,所以defer语句中修改i值,对函数返回值不造成影响,最终返回1
  30. */
  31. }
  32. // 主函数拥有具名返回值
  33. /**
  34. 主函数声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。
  35. */
  36. func foo2() (ret int) {
  37. defer func() {
  38. ret++
  39. }()
  40. return 0
  41. /**
  42. 上面的函数拆解出来,如下所示:
  43. ret = 0
  44. ret++
  45. return
  46. 函数真正返回前,在defer中对返回值做了+1操作,所以函数最终返回1
  47. */
  48. }
  49. func main() {
  50. fmt.Println(deferFuncReturn()) // 2
  51. fmt.Println(foo()) // 1
  52. fmt.Println(foo2()) // 1
  53. }

异常情况 (finally)

defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码

  1. func main() {
  2. fmt.Println("test 1111")
  3. defer fmt.Println("test defer 1111")
  4. defer fmt.Println("test defer 2222")
  5. panic("")
  6. fmt.Println("test 2222")
  7. /**
  8. test 1111
  9. test defer 2222
  10. test defer 1111
  11. panic:
  12. goroutine 1 [running]:
  13. main.main()
  14. go-test/defer.go:7 +0xf8
  15. Process finished with the exit code 2
  16. */
  17. }

defer的实现原理

1.defer数据结构

  1. type _defer struct {
  2. sp uintptr //函数栈指针
  3. pc uintptr //程序计数器
  4. fn *funcval //函数地址
  5. link *_defer //指向自身结构的指针,用于链接多个defer
  6. }

我们知道defer后面一定要接一个函数的,所以defer的数据结构根一般函数类似,也有栈指针、程序计数器、函数地址等等。

与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer就从单链表表头取出一个defer执行。

image.png

从上图可以看到,新声明的defer总是添加到链表头部。

函数返回前执行defer则是从链表首部依次取出执行,不再赘述。

一个goroutine可能连续调用多个函数,defer添加过程跟上述流程一致,进入函数时添加defer,离开函数时取出defer,所以即便调用多个函数,也总是能保证defer是按FIFO方式执行的。

2.defer的创建和执行

源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明defer处调用,将其defer函数存入goroutine的链表中;
  • deferreturn(): 在return指令,准确的讲是在ret指令前调用,将其defer从goroutine链表中取出并执行

可以这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()
[

](https://blog.csdn.net/zhongcanw/article/details/89918343)

defer 表达式的使用场景

defer 通常用于 open/close, connect/disconnect, lock/unlock 等这些成对的操作, 来保证在任何情况下资源都被正确释放.
例如:

  1. var mutex sync.Mutex
  2. var count = 0
  3. func increment() {
  4. mutex.Lock()
  5. defer mutex.Unlock()
  6. count++
  7. }

在increment 函数中, 我们为了避免竞态条件的出现, 而使用了 Mutex 进行加锁. 而在进行并发编程时, 加锁了却忘记(或某种情况下 unlock 没有被执行), 往往会造成灾难性的后果. 为了在任意情况下, 都要保证在加锁操作后, 都进行对应的解锁操作, 我们可以使用 defer 调用解锁操作.

参考

https://blog.csdn.net/zhongcanw/article/details/89918343
https://blog.csdn.net/huang_yong_peng/article/details/82950743
https://segmentfault.com/a/1190000006823652
https://zhuanlan.zhihu.com/p/63354092