考察defer和panic执行顺序的问题

题目来源: 小米

答案:

例子

  1. func main(){
  2. call()
  3. fmt.Println("333 Helloworld")
  4. }
  5. func call() {
  6. defer func(){
  7. fmt.Println("11111")
  8. }()
  9. defer func(){
  10. fmt.Println("22222")
  11. }()
  12. defer func() {
  13. if r := recover(); r!= nil {
  14. fmt.Println("Recover from r : ",r)
  15. }
  16. }()
  17. defer func(){
  18. fmt.Println("33333")
  19. }()
  20. fmt.Println("111 Helloworld")
  21. panic("Panic 1!")
  22. // 这个panic和后面的代码已经不会执行,ide应该会有提示
  23. panic("Panic 2!")
  24. fmt.Println("222 Helloworld")
  25. }
  26. // output
  27. 111 Helloworld
  28. 33333
  29. Recover from r : Panic 1!
  30. 22222
  31. 11111
  32. 333 Helloworld

源码实现

  1. // panic 关键字代码实现.
  2. func gopanic(e interface{}) {
  3. gp := getg() // getg()返回当前协程的 g 结构体指针,g 结构体描述 goroutine
  4. if gp.m.curg != gp {
  5. print("panic: ")
  6. printany(e)
  7. print("\n")
  8. throw("panic on system stack")
  9. }
  10. // 省略无用代码.
  11. var p _panic
  12. p.arg = e
  13. p.link = gp._panic
  14. gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
  15. atomic.Xadd(&runningPanicDefers, 1)
  16. for {
  17. // 获取当前协程的defer链表.
  18. // 这里defer链表和在代码定义的顺序是相反的,类似于先进后出的概念.
  19. d := gp._defer
  20. if d == nil {
  21. break // 当前协程的defer都被执行后,defer链表为空,此时退出for循环
  22. }
  23. // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
  24. // take defer off list. The earlier panic or Goexit will not continue running.
  25. if d.started { // 发生panic后,在defer中又遇到panic(),则会进入这个代码块
  26. if d._panic != nil {
  27. d._panic.aborted = true
  28. }
  29. d._panic = nil
  30. d.fn = nil
  31. gp._defer = d.link
  32. freedefer(d) // defer 已经被执行过,则释放这个defer,继续for循环。
  33. continue
  34. }
  35. // Mark defer as started, but keep on list, so that traceback
  36. // can find and update the defer's argument frame if stack growth
  37. // or a garbage collection happens before reflectcall starts executing d.fn.
  38. d.started = true
  39. // Record the panic that is running the defer.
  40. // If there is a new panic during the deferred call, that panic
  41. // will find d in the list and will mark d._panic (this panic) aborted.
  42. d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
  43. p.argp = unsafe.Pointer(getargp(0))
  44. reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) // 执行当前协程defer链表头的defer
  45. p.argp = nil
  46. // reflectcall did not panic. Remove d.
  47. if gp._defer != d {
  48. throw("bad defer entry in panic")
  49. }
  50. d._panic = nil
  51. d.fn = nil
  52. gp._defer = d.link // 从defer链中移除刚刚执行过的defer
  53. // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
  54. //GC()
  55. pc := d.pc
  56. sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
  57. freedefer(d) // 释放刚刚执行过的defer
  58. if p.recovered { // defer()中遇到recover后进入这个代码块
  59. atomic.Xadd(&runningPanicDefers, -1)
  60. gp._panic = p.link
  61. mcall(recovery) // 跳转到recover()处,继续往下执行
  62. throw("recovery failed") // mcall should not return
  63. }
  64. }
  65. // ran out of deferred calls - old-school panic now
  66. // Because it is unsafe to call arbitrary user code after freezing
  67. // the world, we call preprintpanics to invoke all necessary Error
  68. // and String methods to prepare the panic strings before startpanic.
  69. preprintpanics(gp._panic)
  70. startpanic()
  71. // startpanic set panicking, which will block main from exiting,
  72. // so now OK to decrement runningPanicDefers.
  73. atomic.Xadd(&runningPanicDefers, -1)
  74. printpanics(gp._panic) // 输出panic信息
  75. dopanic(0) // should not return
  76. *(*int)(nil) = 0 // not reached
  77. }

概括

大概的流程就是,如果遇见panic关键字的话,那么go执行器就会进入代码gopanic函数中,进入之后会拿到表示当前协程g的指针,然后通过该指针拿到当前协程的defer链表,通过for循环来进行执行defer,如果在defer中又遇见了panic的话,则会释放这个defer,通过continue去执行下一个defer,然后就是一个一个的执行defer了,如果在defer中遇见recover,那么将会通过mcall(recovery)去执行panic.

参考资料

https://juejin.cn/post/6974298479065563166