走进云原生 走进云原生
首页
走进Kubernetes
  • bash shell
  • 分类
  • 标签
  • 归档
关于
友情链接
GitHub (opens new window)

幕轻寒(xiaomudk)

首页
走进Kubernetes
  • bash shell
  • 分类
  • 标签
  • 归档
关于
友情链接
GitHub (opens new window)
  • Golang的defer和recover

    • defer
      • recover
      幕轻寒
      2019-02-05
      随笔
      目录

      Golang的defer和recover

      Python中提供了with表达式可以很直观、方便地进行应用上下文资源的管理,在代码块执行结束、抛出异常时会自动处理资源的释放、清理操作。

      with open('/etc/passwd', 'r') as f:
          for line in f:
              print line
      
      1
      2
      3

      上述代码在with代码块内执行完毕、触发异常后会自动调用f的__exit__方法,进行文件的关闭操作。

      Python的with实现很直观、方便,Golang中提供了defer、recover用来实现类似的功能。

      # defer

      defer语句会在方法执行完毕前、return之前、或者对应的goroutine时panic时调用defer后面的方法。

      注意:

      1. defer只能用在方法、函数内。

      错误示例:

      package main
      
      import (
          "fmt"
      )
      
      defer func test() {}() // prog.go:6:1: syntax error: non-declaration statement outside function body
      
      func main() {
          fmt.Println(t())
      }
      
      func t() int{
          i:=0
          defer func (){}()
          return i
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      1. defer后的表达式必须是function或者method的调用。

        错误示例:

        func t() int{
            i:=0
            defer i++ // prog.go:14:10: expression in defer must be function call
            return i
        }
        
        1
        2
        3
        4
        5
      2. defer会忽略方法调用的返回值, defer调用内置方法时需要遵守expression statement规范,defer后不能调用append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof方法。

      3. defer按照逆序执行,及后定义的defer方法先于前面定义的defer方法执行。

      4. defer只会在当前的goroutine中调用。

      5. defer后的函数调用参数是在defer语句定义时确定的,defer后的方法调用可能会修改方法返回值,谨记: return语句不是原子指令。

      在不执行代码的情况下,猜想下列方法的返回值:

      package main
      
      import "fmt"
      
      func main() {
      	fmt.Printf("test1 result %d\n", test1())
      	fmt.Printf("test1 result %d\n", test2())
      	fmt.Printf("test1 result %d\n", test3())
      	fmt.Printf("test1 result %d\n", test4())
      }
      
      func test1() int {
      	r := 0
      	defer func() {
      		r = r + 1
      	}()
      	return r
      }
      
      func test2() (r int) {
      	r = 0
      	defer func() {
      		r = r + 1
      	}()
      	return
      }
      
      func test3() (r int) {
      	r = 5
      	defer func(r int) {
      		r = r + 5
      	}(r)
      	return
      }
      
      func test4() (r int) {
      	r = 5
      	defer func(r int) {
      		r = r + 5
      	}(r)
      	return 1
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43

      执行后输出:

      test1 result 0
      test1 result 1
      test1 result 5
      test1 result 1
      
      1
      2
      3
      4

      test1很容易理解,与下面代码等价

      func test1() (result int) {
          r := 0
          result = r
          func() {
              r = r + 1
          }
          return
      }
      
      1
      2
      3
      4
      5
      6
      7
      8

      test2可以写成

      func test2() (r int) {
          r = 0
      	func() {
      		r = r + 1
      	}()
      	return
      }
      
      1
      2
      3
      4
      5
      6
      7

      test3因为defer后的方法调用参数值在defer定义时已确定,形参r的值为r的一个值拷贝, 因而可以替换为

      func test3() (r int) {
      	r = 5
      	func(r int) {
      		r = r + 5
      	}(r)
      	return
      }
      
      1
      2
      3
      4
      5
      6
      7

      test4中的return 1先给返回值r赋值为1,然后执行defered函数,可以等价为

      func test4() (r int) {
      	r = 5
          r = 1
      	func(r int) {
      		r = r + 5
      	}(5)  // r在defer定义时的值为5,在return之前才被赋值为1
      	return
      }
      
      1
      2
      3
      4
      5
      6
      7
      8

      # recover

      go中定义了两个方法

      func panic(interface{}) // 触发异常
      func recover() interface{} // 捕获异常并处理
      
      1
      2

      使用recover()方法可以捕获显式地用panic方法引发的异常或系统的运行时异常(如数组下标越界、nil方法调用),调用panic方法时可以提供参数以传递给recover()。

      recover()方法在下列情况下会返回nil:

      1. 调用panic时没有参数

      2. 没有通过defer的function调用(注意是function,直接defer recover()是不起作用的),所以recover只有用在defer的function时才会起作用。

      3. 当前goroutine没有panic,即是说不是当前goroutine引发的panic不会被defer和defer中的recover捕获和处理。

      package main
      
      import (
      	"fmt"
      )
      
      var sem chan int = make(chan int)
      
      func main() {
      	defer func() {
      		if e := recover(); e != nil {
      			fmt.Printf("Catch panic: %v\n", e)
      		}
      	}()
      	go t()
      	<-sem
      	fmt.Println("done")
      }
      
      // t is just a test method
      // No recover used in this function, so a panic will throw and cause the top function panic and stop
      func t() {
      	defer func() {
      		sem <- 1
      	}()
      	panic("I'm panic")
      }
      /** output
       panic不能被main中的defer捕获并处理,导致main也因为panic退出,所以最后的done没有打印出来
      panic: I'm panic
      
      goroutine 5 [running]:
      main.t()
      	/tmp/sandbox093977267/main.go:26 +0x60
      created by main.main
      	/tmp/sandbox093977267/main.go:15 +0x60
       **/
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37

      注意:

      1. 在panic火runtime error触发后,如果当前的异常没有被处理,或者是在recover()后继续抛出异常,异常会一直在当前的goroutine中向上 传递,直到被defer方法调用处理或因为在当前goroutine中没法被处理,导致顶层的goroutine因为panic退出(注意顶层的goutine并不能通过recover捕获处理该panic,但panic会导致顶层调用退出)。

      2. 即使使用recover捕获异常并正常处理,原有的代码执行已经终止,在panic或异常触发后的代码都不会被执行,但后续的defer方法还会被调用。

        如下所示:

        package main
        
        import (
            "fmt"
        )
        
        func main() {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Printf("Catch panic from f1: %v\n", r)
                }
            }()
            f1()
        }
        
        func f1() {
            defer func() {
                fmt.Println("last defer")
            }()
            defer func() {
                if r := recover(); r != nil {
                    fmt.Printf("Catch panic in f1: %v\n", r)
                    fmt.Println("Will repanic")
                    panic(r)
                }
            }()
            // 不能捕捉到异常
            defer recover()
            panic("oops")
        }
        
        /** output
        Catch panic in f1: oops
        Will repanic
        last defer
        Catch panic from f1: oops
        **/
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37

      # 源码分析

      先来看看recover的源码,recover的具体实现在runtime包中:

      // runtine/panic.go
      
      // The implementation of the predeclared function recover.
      // Cannot split the stack because it needs to reliably
      // find the stack segment of its caller.
      //
      // TODO(rsc): Once we commit to CopyStackAlways,
      // this doesn't need to be nosplit.
      //go:nosplit
      func gorecover(argp uintptr) interface{} {
      	// Must be in a function running as part of a deferred call during the panic.
      	// Must be called from the topmost function of the call
      	// (the function used in the defer statement).
      	// p.argp is the argument pointer of that topmost deferred function call.
      	// Compare against argp reported by caller.
      	// If they match, the caller is the one who can recover.
      	gp := getg()
      	p := gp._panic
      	if p != nil && !p.recovered && argp == uintptr(p.argp) {
      		p.recovered = true
      		return p.arg
      	}
      	return nil
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24

      通过getg()获取当前的goroutine,并获取当前的_panic信息,如果当前goroutine存在panic并且没有被标记为recovered,则将该panic标志为recovered并返回panic的参数。

      从代码中看到recover()只将当前goroutine的panic标记为recovered并返回panic的参数,但是并没有看到相关的代码跳转、函数调用。

      这时可以看下panic的实现,panic也是在runtime包中实现。

      // runtine/panic.go
      
      // The implementation of the predeclared function panic.
      func gopanic(e interface{}) {
         gp := getg()
         // ...
      
         for {
         	d := gp._defer
         	if d == nil {
         		break
         	}
      
         	// ...
             
             gp._defer = d.link
             
             // ...
             
         	pc := d.pc
         	sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
         	freedefer(d)
         	if p.recovered {
         		atomic.Xadd(&runningPanicDefers, -1)
      
         		gp._panic = p.link
                 
                 // ...
                 
         		// Pass information about recovering frame to recovery.
         		gp.sigcode0 = uintptr(sp)
         		gp.sigcode1 = pc
         		mcall(recovery)
         		throw("recovery failed") // mcall should not return
         	}
         }
      
         // ...
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39

      在调用panic时,会遍历当前goroutine中的_defer链表,直到deferred执行完毕,如果当前goroutine的panic被标记为recovered,则将当前goroutine的_panic指向上一个panic(gp._panic = p.link),通过mcall(recovery)记录下当前的pc,sp信息、进入defer上下文执行defer,通过recovery使得方法正常返回(移除该panic)。

      现在来看下defer的实现

      // runtime/panic.go
      
      
      
      // Create a new deferred function fn with siz bytes of arguments.
      // The compiler turns a defer statement into a call to this.
      //go:nosplit
      func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
         if getg().m.curg != getg() {
         	// go code on the system stack can't defer
         	throw("defer on system stack")
         }
      
         // the arguments of fn are in a perilous state. The stack map
         // for deferproc does not describe them. So we can't let garbage
         // collection or stack copying trigger until we've copied them out
         // to somewhere safe. The memmove below does that.
         // Until the copy completes, we can only call nosplit routines.
         sp := getcallersp()
         argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
         callerpc := getcallerpc()
      
         d := newdefer(siz)
         if d._panic != nil {
         	throw("deferproc: d.panic != nil after newdefer")
         }
         d.fn = fn
         d.pc = callerpc
         d.sp = sp
         
         // ...
      
         // deferproc returns 0 normally.
         // a deferred func that stops a panic
         // makes the deferproc return 1.
         // the code the compiler generates always
         // checks the return value and jumps to the
         // end of the function if deferproc returns != 0.
         return0()
         // No code can go here - the C return register has
         // been set and must not be clobbered.
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42

      defer关键字通过调用deferproc往goroutine的defer链中添加defer调用,return0()会调用deferreturn,正常情况下deferproc会返回0,如果在deferproc中处理panic,会返回1,这也是gopanic中调用recovery方法的作用,recovery方法将结果设置为1,这时会跳转到代码的return之前执行其余的deferproc方法。

      // runtine/panic.go
      
      // Run a deferred function if there is one.
      // The compiler inserts a call to this at the end of any
      // function which calls defer.
      // If there is a deferred function, this will call runtime·jmpdefer,
      // which will jump to the deferred function such that it appears
      // to have been called by the caller of deferreturn at the point
      // just before deferreturn was called. The effect is that deferreturn
      // is called again and again until there are no more deferred functions.
      // Cannot split the stack because we reuse the caller's frame to
      // call the deferred function.
      
      // The single argument isn't actually used - it just has its address
      // taken so it can be matched against pending defers.
      //go:nosplit
      func deferreturn(arg0 uintptr) {
         gp := getg()
         d := gp._defer
         if d == nil {
         	return
         }
      
         // ...
         
         fn := d.fn
         d.fn = nil
         gp._defer = d.link
         freedefer(d)
         jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31

      deferreturn每执行一次会将执行完的defer从goroutine的_defer调用链中移除,执行到jmpdefer会重新进入deferreturn方法中执行直到没有gp._defer为nil, 这时所有defer执行完毕。

      #Golang
      上次更新: 2022/06/20, 15:31:59
      Theme by Vdoing | Copyright © 2019-2023 | 幕轻寒(xiaomudk) | 赞助商:
      • 跟随系统
      • 浅色模式
      • 深色模式
      • 阅读模式