关于官方解释

  1. A defer statement defers the execution of a function until the surrounding function returns.
  2. The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

这里提到了defer调用的参数会立即计算,但在周围函数返回之前不会执行函数调用。

以及延迟函数调用被压入堆栈。当函数返回时,其延迟调用以后进先出顺序执行。

像这样一段代码

  1. func A(){
  2. defer B()
  3. // code to do something
  4. }

编译后的伪指令是这样的

  1. func f1() {
  2. r := runtime.deferproc(0, A) // 经过recover返回时r为1,否则为0
  3. if r > 0 {
  4. goto ret
  5. }
  6. // code to do something
  7. runtime.deferreturn()
  8. return
  9. ret:
  10. runtime.deferreturn()
  11. }

其中与defer指令相关的有两个部分。第一部分是deferproc,它负责保存要执行的函数信息,我们称之为defer“注册”

  1. func deferproc(siz int32, fn *funcval)

从函数原型来看,deferproc函数有两个参数,第一个是被注册的defer函数的参数加返回值共占多少字节;第二个参数是一个runtime.funcval结构体的指针,也就是一个Function Value。

与defer指令相关的第二部分就是deferreturn,它被编译器插入到函数返回以前调用,负责执行已经注册的defer函数。所以defer函数之所以能延迟到函数返回前执行,就是因为先注册,后调用。

defer信息会注册到一个链表,而当前执行的goroutine持有这个链表的头指针,每个goroutine在运行时都有一个对应的结构体g、其中有一个字段,指向defer链表头,defer链表链接起来的是一个一个 _defer结构体,新注册的defer会添加到链表头,执行时也是从头开始,所以defer才会表现为倒序执行。

image.png

image.png

defer结构

  1. type _defer struct {
  2. siz int32
  3. started bool
  4. sp uintptr // sp at time of defer
  5. pc uintptr
  6. fn *funcval
  7. _panic *_panic // panic that is running defer
  8. link *_defer
  9. }

siz:由deferproc第一个参数传入,就是defer函数参数加返回值的总大小。这段空间会直接分配在_defer结构体后面,用于在注册时保存给defer函数传入的参数,并在执行时直接拷贝到defer函数的调用者栈上。

started :标识defer函数是否已经开始执行;

sp:就是注册defer函数的函数栈指针;

pc:是deferproc函数返回后要继续执行的指令地址;

fn:由deferproc的第二个参数传入,也就是被注册的defer函数;

_panic:是触发defer函数执行的panic指针,正常流程执行defer时它就是nil;

link:自然是链到之前注册的那个_defer结构体。

defer函数的参数是如何传递的。

  1. func A1(a int) {
  2. fmt.Println(a)
  3. }
  4. func A() {
  5. a, b := 1, 2
  6. defer A1(a)
  7. a = a + b
  8. fmt.Println(a, b)
  9. }

这个例子中,函数A注册了一个defer函数A1,在A的函数栈帧中,局部变量区域存储a=1,b=2
defer - 图3

到deferproc函数注册defer函数A1时:

  1. func deferproc(siz int32, fn *funcval)

第一个参数是A1的参数加返回值共占多少字节。A1没有返回值,64位下一个整型参数占用8字节。

第二个参数是函数A1。没有捕获列表的Function Value,在编译阶段会做出优化,就是在只读数据段分配一个共用的funcval结构体。

如下图中,函数A1的指令入口地址为addr1。在只读数据段分配的指向A1指令入口的funcval结构体地址为addr2,所以deferproc函数第二个参数就是addr2。
defer - 图4

额外要注意的是,deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。

A1只有一个参数a=1,放在deferproc函数自己的两个参数之后。注意deferproc函数的返回值空间并没有分配在调用者栈上,而是放到了寄存器中,这和recover有关

deferproc函数执行,需要堆分配一段空间,用于放_defer结构体以及后面siz大小的参数与返回值。
_defer结构体的第一个字段,A1的参数加返回值共占8字节;defer函数尚未执行,所以started=false;sp就是调用者A的栈指针;pc就是deferproc函数的返回地址return addr;被注册的function value为A1;defer结构体后面的8字节用来保存传递给A1的参数。

image.png
defer函数的参数怎么传给deferproc

deferproc函数执行,需要堆分配一段空间,用于放_defer结构体以及后面siz大小的参数与返回值。
_defer结构体的第一个字段,A1的参数加返回值共占8字节;defer函数尚未执行,所以started=false;sp就是调用者A的栈指针;pc就是deferproc函数的返回地址return addr;被注册的function value为A1;defer结构体后面的8字节用来保存传递给A1的参数。

image.png

要注册的defer链表项

然后这个defer结构体就被添加到defer链表头,deferproc注册结束。
频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。
**
按照如下函数A编译后的伪指令所示,deferproc结束后,接下来会执行到a=a+b这一步,所以,局部变量a被置为3。接下来会输出:a=3,b=2。

  1. //函数A编译后的伪指令
  2. func A() {
  3. a, b := 1, 2
  4. r := runtime.deferproc(8, A11)
  5. if r > 0 {
  6. goto ret
  7. }
  8. a = a + b
  9. fmt.Println(a, b)//3,2
  10. runtime.deferreturn()//执行defer链表
  11. return
  12. ret:
  13. runtime.deferreturn()
  14. }

然后就到deferreturn执行defer链表这里了。从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。

image.png

调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输出参数值a=1。
这个例子就到这里,关键是理解defer函数的参数在注册时拷贝到堆上,执行时再拷贝到栈上。

既然deferproc注册的是一个Function Value,而这次的例子是没有捕获列表的Function Value

defer 特性:

  1. 1. 关键字 defer 用于注册延迟调用。
  2. 2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 3. 多个defer语句,按先进后出的方式执行。
  4. 4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 1. 关闭文件句柄
  2. 2. 锁资源释放
  3. 3. 数据库连接释放

defer指令对应着2部分

其中deferproc负责把执行的函数信息保存起来—defer注册
直到返回之前通过deferreturn执行注册的defer函数,先注册后调用,才实现了延迟调用的效果

defer信息会注册到一个链表,而当前执行的goroutine持有当前这个链表的头指针

它有如何特点

  • 所在的函数中,它在 return 或 panic 或 执行完毕 后被调用
  • 多个 defer,它们的被调用顺序,为栈的形式。先进后出,先定义的后被调用

看下面几个例子:

  1. 在计算defer语句时,将计算延迟函数的参数。在此示例中,在延迟Println调用时计算表达式“i”。函数返回后,延迟调用将打印“0”。

    1. func a() {
    2. i := 0
    3. defer fmt.Println(i)
    4. i++
    5. return
    6. }
  2. 在周围函数返回后,延迟函数调用以后进先出顺序执行。

  1. func b() {
  2. for i := 0; i < 4; i++ {
  3. defer fmt.Print(i)
  4. }
  5. } //将会打印3210

然后不免在使用过程中会遇到这些坑

坑1. defer在匿名返回值和命名返回值函数中的不同表现

  1. func returnValues() int {
  2. var result int
  3. defer func() {
  4. result++
  5. fmt.Println("defer")
  6. }()
  7. return result
  8. }
  9. func namedReturnValues() (result int) {
  10. defer func() {
  11. result++
  12. fmt.Println("defer")
  13. }()
  14. return result
  15. }
  16. func main() {
  17. var whatever [5]struct{}
  18. for i := range whatever {
  19. defer func() { fmt.Println(i) }()
  20. }
  21. }

下面的方法输出4,4,4,4,4

上面的方法会输出0,中间的方法输出1。上面的方法使用了匿名返回值,中间的使用了命名返回值,除此之外其他的逻辑均相同,为什么输出的结果会有区别呢?

要搞清这个问题首先需要了解defer的执行逻辑,defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。

  • 将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
  • 然后检查是否有defer,如果有则执行
  • 返回刚才创建的返回值(retValue)

在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。

坑2. 判断执行没有err之后,再defer释放资源

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确做法

  1. resp, err := http.Get(url)
  2. // 先判断操作是否成功
  3. if err != nil {
  4. return err
  5. }
  6. // 如果操作成功,再进行Close操作
  7. defer resp.Body.Close()

坑3. 调用os.Exit时defer不会被执行
当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

  1. func deferExit() {
  2. defer func() {
  3. fmt.Println("defer")
  4. }()
  5. os.Exit(0)
  6. }

上面的defer并不会输出。

坑4.非引用传参给defer调用的函数,且为非闭包函数,值不会受后面的改变影响

  1. func defer0() {
  2. a := 1 // a 作为演示的参数
  3. defer fmt.Println(a) // 非引用传参,非闭包函数中,a 的值 不会 受后面的改变影响
  4. a = a + 2
  5. }
  6. // 控制台输出 1

坑5. 传递引用给defer调用的函数,即使不使用闭包函数,值也会受后面的改变影响

  1. func myPrintln(point *int) {
  2. fmt.Println(*point) // 输出引用所指向的值
  3. }
  4. func defer1() {
  5. a := 3
  6. // &a a 的引用。内存中的形式: 0x .... ---> 3
  7. defer myPrintln(&a) // 传递引用给函数,即使不使用闭包函数,值 受后面的改变影响
  8. a = a + 2
  9. }
  10. // 控制台输出 5

坑6. 传递值给defer调用的函数,且非闭包函数,值不会受后面的改变影响

  1. func p(a int) {
  2. fmt.Println(a)
  3. }
  4. func defer2() {
  5. a := 3
  6. defer p(a) // 传递值给函数,且非闭包函数,值 不会 受后面的改变影响
  7. a = a + 2
  8. }
  9. // 控制台输出: 3

坑7. defer调用闭包函数,且内调用外部非传参进来的变量,值会受后面的改变影响

  1. // 闭包函数内,事实是该值的引用
  2. func defer3() {
  3. a := 3
  4. defer func() {
  5. fmt.Println(a) // 闭包函数内调用外部非传参进来的变量,事实是该值的引用,值 会 受后面的改变影响
  6. }()
  7. a = a + 2 // 3 + 2 = 5
  8. }
  9. // 控制台输出: 5

坑8. defer调用闭包函数,若内部使用了传参参数的值。使用的是值

  1. func defer5() {
  2. a := []int{1,2,3}
  3. for i:=0;i<len(a);i++ {
  4. // 闭包函数内部使用传参参数的值。内部的值为传参的值。同时引用是不同的
  5. defer func(index int) {
  6. // index 有一个新地址指向它
  7. fmt.Println(a[index]) // index == i
  8. }(i)
  9. // 后进先出,3 2 1
  10. }
  11. }
  12. // 控制台输出:
  13. // 3
  14. // 2
  15. // 1

坑9. defer所调用的非闭包函数,参数如果是函数,会按顺序先执行(函数参数)

  1. func calc(index string, a, b int) int {
  2. ret := a + b
  3. fmt.Println(index, a, b, ret)
  4. return ret
  5. }
  6. func defer6() {
  7. a := 1
  8. b := 2
  9. // calc 充当了函数中的函数参数。即使在 defer 的函数中,它作为函数参数,定义的时候也会首先调用函数进行求值
  10. // 按照正常的顺序,calc("10", a, b) 首先被调用求值。calc("122", a, b) 排第二被调用
  11. defer calc("1", a, calc("10", a, b))
  12. defer calc("12",a, calc("122", a, b))
  13. }
  14. // 控制台输出:
  15. /**
  16. 10 1 2 3 // 第一个函数参数
  17. 122 1 2 3 // 第二个函数参数
  18. 12 1 3 4 // 倒数第一个 calc
  19. 1 1 3 4 // 倒数第二个 calc
  20. */

defer与return谁先谁后

  1. package main
  2. import "fmt"
  3. func deferFunc() int {
  4. fmt.Println("defer func called")
  5. return 0
  6. }
  7. func returnFunc() int {
  8. fmt.Println("return func called")
  9. return 0
  10. }
  11. func returnAndDefer() int {
  12. defer deferFunc()
  13. return returnFunc()
  14. }
  15. func main() {
  16. returnAndDefer()
  17. }
  1. return func called
  2. defer func called

结论为:return之后的语句先执行,defer后的语句后执行

遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中:遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. defer_call()
  7. fmt.Println("main 正常结束")
  8. }
  9. func defer_call() {
  10. defer func() { fmt.Println("defer: panic 之前1") }()
  11. defer func() { fmt.Println("defer: panic 之前2") }()
  12. panic("异常内容") //触发defer出栈
  13. defer func() { fmt.Println("defer: panic 之后,永远执行不到") }()
  14. }
  1. defer: panic 之前2
  2. defer: panic 之前1
  3. panic: 异常内容
  4. //... 异常堆栈信息

defer 碰上闭包

  1. package main
  2. import "fmt"
  3. func main() {
  4. var whatever [5]struct{}
  5. for i := range whatever {
  6. defer func() { fmt.Println(i) }()
  7. }
  8. }

输出 4,4,4,4,4

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

  1. package main
  2. func test(x int) {
  3. defer println("a")
  4. defer println("b")
  5. defer func() {
  6. println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
  7. }()
  8. defer println("c")
  9. }
  10. func main() {
  11. test(0)
  12. }

输出

  1. c
  2. b
  3. a
  4. panic: runtime error: integer divide by zero

延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

  1. package main
  2. func test() {
  3. x, y := 10, 20
  4. defer func(i int) {
  5. println("defer:", i, y) // y 闭包引用
  6. }(x) // x 被复制
  7. x += 10
  8. y += 100
  9. println("x =", x, "y =", y)
  10. }
  11. func main() {
  12. test()
  13. }

输出结果

  1. x = 20 y = 120
  2. defer: 10 120

滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var lock sync.Mutex
  8. func test() {
  9. lock.Lock()
  10. lock.Unlock()
  11. }
  12. func testdefer() {
  13. lock.Lock()
  14. defer lock.Unlock()
  15. }
  16. func main() {
  17. func() {
  18. t1 := time.Now()
  19. for i := 0; i < 10000; i++ {
  20. test()
  21. }
  22. elapsed := time.Since(t1)
  23. fmt.Println("test elapsed: ", elapsed)
  24. }()
  25. func() {
  26. t1 := time.Now()
  27. for i := 0; i < 10000; i++ {
  28. testdefer()
  29. }
  30. elapsed := time.Since(t1)
  31. fmt.Println("testdefer elapsed: ", elapsed)
  32. }()
  33. }

输出

  1. test elapsed: 223.162µs
  2. testdefer elapsed: 781.304µs

defer 与 closure

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. func foo(a, b int) (i int, err error) {
  7. defer fmt.Printf("first defer err %v\n", err)
  8. defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
  9. defer func() { fmt.Printf("third defer err %v\n", err) }()
  10. if b == 0 {
  11. err = errors.New("divided by zero!")
  12. return
  13. }
  14. i = a / b
  15. return
  16. }
  17. func main() {
  18. foo(2, 0)
  19. }

输出

  1. third defer err divided by zero!
  2. second defer err <nil>
  3. first defer err <nil>

defer 与 return

  1. package main
  2. import "fmt"
  3. func foo() (i int) {
  4. i = 0
  5. defer func() {
  6. fmt.Println(i)
  7. }()
  8. return 2
  9. }
  10. func main() {
  11. foo()
  12. }

输出 2

在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。

注意

  • defer 不影响 return的值
  • defer 最大的功能是 Panic 后依然有效

练习题

  1. package main
  2. import "fmt"
  3. func DeferFunc1(i int) (t int) {
  4. t = i
  5. defer func() {
  6. t += 3
  7. }()
  8. return t
  9. }
  10. // 将返回值t赋值为传入的i,此时t为1
  11. // 执行return语句将t赋值给t(等于啥也没做)
  12. // 执行defer方法,将t + 3 = 4
  13. // 函数返回 4
  14. // 因为t的作用域为整个函数所以修改有效。
  15. func DeferFunc2(i int) int {
  16. t := i
  17. defer func() {
  18. t += 3
  19. }()
  20. return t
  21. }
  22. // 创建变量t并赋值为1
  23. // 执行return语句,注意这里是将t赋值给返回值,此时返回值为1(这个返回值并不是t)
  24. // 执行defer方法,将t + 3 = 4
  25. // 函数返回返回值1
  26. func DeferFunc3(i int) (t int) {
  27. defer func() {
  28. t += i
  29. }()
  30. return 2
  31. }
  32. func DeferFunc4() (t int) {
  33. defer func(i int) {
  34. fmt.Println(i)
  35. fmt.Println(t)
  36. }(t)
  37. t = 1
  38. return 2
  39. }
  40. // 初始化返回值t为零值 0
  41. // 首先执行defer的第一步,赋值defer中的func入参t为0
  42. // 执行defer的第二步,将defer压栈
  43. // 将t赋值为1
  44. // 执行return语句,将返回值t赋值为2
  45. // 执行defer的第三步,出栈并执行
  46. // 因为在入栈时defer执行的func的入参已经赋值了,此时它作为的是一个形式参数,所以打印为0;相对应的因为最后已经将t的值修改为2,所以再打印一个2
  47. func main() {
  48. fmt.Println(DeferFunc1(1))
  49. fmt.Println(DeferFunc2(1))
  50. fmt.Println(DeferFunc3(1))
  51. DeferFunc4()
  52. }

参考