一文初探Go的defer、panic和recover

3,476 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7天,点击查看活动详情

一块来学习一下Go的defer、panic和recover的常规用法,以及深度解析容易掉入的陷阱,看看如何规避。

defer

Go语言的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行(类似数据结构,先进后出)

用途:非常适合用于处理资源释放问题,比如:资源清理、文件关闭、解锁及记录时间等。

简单示例

func main() {
  fmt.Println("start")
  defer fmt.Println(111)
  defer fmt.Println(222)
  defer fmt.Println(333)
  fmt.Println("end")

  //输出:
  //start
  //end
  //333
  //222
  //111
}

执行时机

Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。如下图所示:

使用一个经典案例,加深下执行时机的理解(思考下结果为什么是5 6 5 5?):

func main() {
  fmt.Println(f1())  //5
  fmt.Println(f2())  //6
  fmt.Println(f3())  //5
  fmt.Println(f4())  //5
}

func f1() int {
  x := 5
  defer func() {
    x++
  }()
  return x
}

func f2() (x int) {
  defer func() {
    x++
  }()
  return 5
}

func f3() (y int) {
  x := 5
  defer func() {
    x++
  }()
  return x
}
func f4() (x int) {
  defer func(x int) {
    x++
  }(x)
  return 5
}

思考过程解析

  • f1(): 执行retrun x,先做返回值赋值为5,然后defer里x++,这时并不会重新给返回值赋值,所以结果是5
  • f2(): 执行retrun 5,先做返回值赋值为5,因为x为返回值命名写法,所以defer里x++,会重新给返回值赋值,所以结果为6;
  • f3(): 执行retrun x,先做返回值赋值为5,然后x做值拷贝给y,后来的defer里x++,不会影响y的值,所以结果为5;
  • f4(): 执行retrun 5,先做返回值赋值为5,然后虽然x为返回值命名写法,但defer里重新声明了x为局部变量做x++,这样的操作不会给返回值重新赋值,所以结果为5。

陷阱

先来看看两个例子,输出什么结果,然后再进一步做陷阱分析

例子1:看看结果输出什么?

func main() {
  x := 1
  y := 2
  defer calcTest("AA", x, calcTest("A", x, y))
  x = 10
  defer calcTest("BB", x, calcTest("B", x, y))
  y = 20
}

func calcTest(index string, a, b int) int {
  ret := a + b
  fmt.Println(index, a, b, ret)
  return ret
}

例子2:上面例子中的defer语句改个写法(使用func(){...}()做嵌套),看看结果输出什么?

func main() {
  x := 1
  y := 2
  defer func() {
    calcTest("AA", x, calcTest("A", x, y))
  }()
  x = 10
  defer func() {
    calcTest("BB", x, calcTest("B", x, y))
  }()
  y = 20
}

两个例子,差异就在于使不使用func(){...}()做嵌套,为什么结果却截然不同呢?

例子1输出结果

A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

例子2输出结果

B 10 20 30
BB 10 30 40
A 10 20 30
AA 10 30 40

陷阱分析

  • 首先,要先记住defer语句后面,如果未使用嵌套写法,这时函数里的参数值都得为确定值,即不会因为后面的修改而影响;如果使用嵌套写法,则不是确定值,即后面修改后会同步生效;
  • 所以,例子1执行顺序:先执行输出A→B,然后defer逆序执行输出BB→AA;例子2执行顺序:直接defer逆序执行,顺序为B→BB→A→AA,每一步带入相应的x y值,即可推导出结果。

panic和recover

Go应用程序执行时遇到panic,是会导致程序崩溃,异常退出panic之后的代码不会被执行。

简单示例

func main() {
  AAA()
  BBB()
  CCC()
}

func AAA() {
  fmt.Println("func AAA")
}
func BBB() {
  panic("func BBB")
}
func CCC() {
  fmt.Println("func CCC")
}

//输出结果:
//func AAA
//panic: func BBB
//
//goroutine 1 [running]:
//main.BBB(...)
//    .../09_func_panic_recover.go:27
//main.main()
//    .../09_func_panic_recover.go:7 +0x96
//exit status 2

为了避免这个“程序崩溃,异常退出”问题,则需要使用recover函数来处理。

func main() {
  AAA()
  BBB()
  CCC()
}

func AAA() {
  fmt.Println("func AAA")
}

func BBB() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("recover in func BBB")
      fmt.Println(fmt.Sprintf("%T %v", err, err))
      //...handle  打日志等
    }
  }()

  panic("func BBB") //panic之前,如果没有recover,则程序会崩溃,异常退出。有则继续执行
}

func CCC() {
  fmt.Println("func CCC")
}

//输出结果:
//func AAA
//recover in func BBB
//string func BBB
//func CCC

注意点

  • recover必须搭配defer来使用,否则panic捕获不到;
  • defer一定要在可能引发panic语句之前定义

陷阱

如果想把recover处理逻辑抽象出来作为通用函数,则在defer语句后面,直接调用函数,切记不要使用 func(){...}()去嵌套调用,因为这样会导致捕捉不到panic。示例如下:

func main() {
  //defer DefaultRecover()  //能捕获panic
  defer func() {            //不能捕获panic
    DefaultRecover()
  }()

  panic("func main")
}

func DefaultRecover() {
  if err := recover(); err != nil {
    fmt.Println("recover in func DefaultRecover")
    fmt.Println(fmt.Sprintf("%T %v", err, err))
    //...handle  打日志等
  }
}

//输出结果:
//panic: func DDD

//goroutine 1 [running]:
//main.DDD()
//        .../09_func_panic_recover.go:46 +0x5b
//main.main()
//        .../09_func_panic_recover.go:10 +0xdc
//exit status 2

如果本文对你有帮助,欢迎点赞收藏加关注,如果本文有错误的地方,欢迎指出!