defer与异常

defer

defer关键字可以让函数或语句延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束、即便已经panic()、即便函数已经return了,也都会执行defer所推迟的对象。

其实defer的本质是,当在某个函数中使用了defer关键字,则创建一个独立的defer栈帧,并将该defer语句压入栈中,同时将其使用的相关变量也拷贝到该栈帧中(显然是按值拷贝的)。因为栈是LIFO方式,所以先压栈的后执行。因为是独立的栈帧,所以即使调用者函数已经返回或报错,也一样能在它们之后进入defer栈帧去执行。

defer的执行顺序

如果语句块内有多个defer,则defer的对象以LIFO(last in first out)的方式执行,也就是说,先定义的defer后执行

  1. fmt.Println("start")
  2. defer fmt.Println(1)
  3. defer fmt.Println(2)
  4. defer fmt.Println(3)
  5. defer fmt.Println(4)
  6. fmt.Println("end")

执行结果:

  1. start
  2. end
  3. 4
  4. 3
  5. 2
  6. 1

defer与匿名函数

  1. fmt.Println("func start")
  2. x := 10
  3. defer func(x int) {
  4. fmt.Println("in defer: ", x)
  5. }(x)
  6. x = 30
  7. fmt.Println("func end: ", x)

因为函数传参是值copy,所以x为10的值在defer定义的时候已经copy传入defer, 后面的修改并不会影响到defer中的值

我们也可以选择把变量的指针传达给defer, 这样外面的修改就是生效的, 例如

  1. fmt.Println("func start")
  2. x := 10
  3. defer func(x *int) {
  4. fmt.Println("in defer: ", *x)
  5. }(&x)
  6. x = 30
  7. fmt.Println("func end: ", x)

defer与闭包

当然最常用的就是直接使用闭包的方式:

  1. fmt.Println("func start")
  2. x := 10
  3. defer func() {
  4. fmt.Println("in defer: ", x)
  5. }()
  6. x = 30
  7. fmt.Println("func end: ", x)

defer的应用

defer有什么用呢?一般用来做善后操作,例如清理垃圾、释放资源,无论是否报错都执行defer对象。另一方面,defer可以让这些善后操作的语句和开始语句放在一起,无论在可读性上还是安全性上都很有改善,毕竟写完开始语句就可以直接写defer语句,永远也不会忘记关闭、善后等操作

异常处理: panic

panic()用于产生错误信息并终止当前的goroutine,一般将其看作是退出panic()所在函数以及退出调用panic()所在函数的函数

  1. func main) {
  2. fn()
  3. }
  4. func fn() {
  5. fmt.Println("start fn")
  6. panic("pannic in fn")
  7. fmt.Println("end fn")
  8. }
  9. // panic: pannic in fn

大部分场景下 panic都不是我们可以预判的, 比如下面

  1. var a *int
  2. fmt.Println(*a)
  3. // panic: runtime error: invalid memory address or nil pointer dereference
  4. // [signal 0xc0000005 code=0x0 addr=0x0 pc=0x4675e6]

由于panic会直接导致程序退出, 一般都不是我们期望的,比如:

  1. func main() {
  2. var x, y *int
  3. sum(x, y)
  4. }
  5. func sum(x, y *int) int {
  6. return *x + *y
  7. }

如果不想panic直接退出程序,我们就需要捕获panic, Go语言内置的recover函数就是干这个的

异常捕获: recover

recover()用于捕捉panic()错误,并返回这个错误信息。但注意,即使recover()捕获到了panic(),但调用含有panic()函数的函数也会退出

比如, 如果我们放前面,由于在这个位置并未panic, 捕获为nil

  1. var x, y *int
  2. fmt.Println(recover())
  3. sum(x, y)

如果 我们放后面,则由于panic提前退出,根本执行不到我们的捕获代码

  1. var x, y *int
  2. sum(x, y)
  3. fmt.Println(recover())

而且我们写程序的时候 也完全预估不了哪里会panic, 所以正确的用法是, 函数调用后 再尝试捕获, 这时候我们就需要使用defer

  1. func main() {
  2. defer func() {
  3. fmt.Println(recover())
  4. }()
  5. var x, y *int
  6. sum(x, y)
  7. }