在正确的位置调用内置recover函数

恐慌/恢复机制已经在前面的文章中介绍了, 另外上一篇文章也介绍了一些恐慌/恢复用例。 通过这些介绍,我们得知一个recover调用只能在一个延迟函数调用中起作用。 但是,并非所有的处于延迟函数调用中的recover调用都能起作用。 本文下面将展示一些不起作用的recover调用,并解释它们为什么不起作用。

我们先看一个例子:
package main

import "fmt"

func main() {
	defer func() {
		defer func() {
			fmt.Println("7:", recover())
		}()
	}()
	defer func() {
		func() {
			fmt.Println("6:", recover())
		}()
	}()
	func() {
		defer func() {
			fmt.Println("1:", recover())
		}()
	}()
	func() {
		defer fmt.Println("2:", recover())
	}()
	func() {
		fmt.Println("3:", recover())
	}()
	fmt.Println("4:", recover())
	defer fmt.Println("5:", recover())
	panic(789)
	defer func() {
		fmt.Println("0:", recover())
	}()
}
运行之,我们将发现上例中的7个recover函数调用都没有恢复程序中产生的恐慌。 此程序的输出结果如下:
1: <nil>
2: <nil>
3: <nil>
4: <nil>
5: <nil>
6: <nil>
7: <nil>
panic: 789

goroutine 1 [running]:
...
显然地,标号为0的recover调用(代码中最后一个)是执行不到的。 其它7个均执行到了,但是它们的返回值都是nil。为什么呢?让我们先阅读一下Go白皮书中列出的规则
在下面的情况下,recover函数调用的返回值为nil
  • 传递给相应panic函数调用的实参为nil;
  • 当前协程并没有处于恐慌状态;
  • recover函数并未直接在一个延迟函数调用中调用。

这里我们忽略第一种情况。上例中标号为1/2/3/4的recover调用都属于第二种情况,标号为5和6的recover调用都属于第三种情况。 但是,白皮书中列出的三种情况都没有解释为什么标号为7的recover调用也没有恢复程序中的恐慌。

我们知道下面的程序中的recover调用将捕获到panic调用抛出的恐慌。
// example2.go
package main

import (
	"fmt"
)

func main() {
	defer func() {
		fmt.Println( recover() ) // 1
	}()

	panic(1)
}

但是,这个简短的程序中的recover调用和前面的例子中的标号为7的recover调用有何本质区别呢?

首先,让我们了解一些概念和事实。

概念:函数调用深度、协程执行深度和恐慌深度

每个函数调用都对应着一个相对于当前协程的入口调用的调用深度。 对于主协程中的一个函数调用,它的调用深度是相对于main入口函数。

用一个例子说明:
package main

func main() { // 深度为0
	go func() { // 深度为0
		func() { // 深度为1
		}()

		defer func() { // 深度为1
			defer func() { // 深度为2
			}()
		}()
	}()

	func () { // 深度为1
		func() { // 深度为2
			go func() { // 深度为0
			}()
		}()

		go func() { // 深度为0
		}()
	}()
}

如果一个协程中的当前执行位置处于某个调用中,则我们说此协程的当前执行深度为此调用的深度。

一个协程的某个恐慌的深度为此恐慌已经传播到的函数调用深度。 请阅读下一节以对恐慌传播有一个清晰的认识。

事实:恐慌只能向更浅的函数深度传播

是的,恐慌只会从一个函数传播到此函数的调用函数,而从不会传播到深度更深的被此函数调用的函数中。
package main

import "fmt"

func main() { // 调用深度为0
	defer func() { // 调用深度为1
		fmt.Println("当前恐慌深度为0(执行深度为1)")
		func() { // 调用深度为2
			fmt.Println("当前恐慌深度为0(执行深度为2)")
			func() { // 调用深度为3
				fmt.Println("当前恐慌深度为0")
			}()
		}()
	}()

	defer fmt.Println("当前恐慌深度为0(执行深度为0)")

	func() { // 调用深度为1
		defer fmt.Println("当前恐慌深度为1(执行深度为1)")
		func() { // 调用深度为2
			defer fmt.Println("当前恐慌深度为2")
			func() { // 调用深度为3
				defer fmt.Println("当前恐慌深度为3")
				panic(1)
			}()
		}()
	}()
}

所以,一个恐慌的深度总是单调递减的,它从不增加。另外,一个协程的恐慌深度从不会小于它的执行深度。

事实:新生成的恐慌将压制同一深度的老的恐慌

一个例子:
package main

import "fmt"

func main() {
	defer fmt.Println("程序退出时未崩溃")

	defer func() {
		fmt.Println( recover() ) // 3
	}()

	defer fmt.Println("恐慌3将压制恐慌2")
	defer panic(3)
	defer fmt.Println("恐慌2将压制恐慌1")
	defer panic(2)
	panic(1)
}
输出:
恐慌2将压制恐慌1
恐慌3将压制恐慌2
3
程序退出时未崩溃

在此例中,恐慌1被恐慌2压制了,恐慌2又被恐慌3压制了。所以,最后被捕获的恐慌值为3。

在一个协程中,任何调用深度上最多只能有一个活动的恐慌共存。 特别地,当一个协程的当前执行深度为0时,此协程中只能存在一个活动的恐慌。

事实:一个协程中可以有多个活动的恐慌共存

一个例子:
package main

import "fmt"

func main() { // 调用深度为0
	defer fmt.Println("程序崩溃了,因为退出时恐慌3依然未恢复")

	defer func() { // 调用深度为1
		defer func() { // 调用深度为2
			// 恐慌6被消除了。
			fmt.Println( recover() ) // 6
		}()

		// 恐慌3的深度为0,恐慌6的深度为1。
		defer fmt.Println("现在,恐慌3和恐慌6共存")
		defer panic(6) // 将压制恐慌5
		defer panic(5) // 将压制恐慌4
		panic(4) // 不会压制恐慌3,因为恐慌4和恐慌3的深度
		         // 不同。恐慌3为0,而恐慌4的深度为1。
	}()

	defer fmt.Println("现在,只存在恐慌3")
	defer panic(3) // 将压制恐慌2
	defer panic(2) // 将压制恐慌1
	panic(1)
}

在这个例子中,两个曾经共存过的恐慌之一(恐慌6)被恢复了。 但是恐慌3在程序退出时仍然没有被恢复,所以此程序在退出时崩溃了。

输出:
现在,只存在恐慌3
现在,恐慌3和恐慌6共存
6
程序崩溃了,因为退出时恐慌3依然未恢复
panic: 1
	panic: 2
	panic: 3

goroutine 1 [running]:
...

事实:更深的恐慌可能被先恢复也可能被后恢复

一个例子:
package main

import "fmt"

func demo(recoverHighestPanicAtFirst bool) {
	fmt.Println("====================")
	defer func() {
		if !recoverHighestPanicAtFirst{
			// 恢复恐慌1
			defer fmt.Println("恐慌", recover(), "被恢复了")
		}
		defer func() {
			//  恢复恐慌2
			fmt.Println("恐慌", recover(), "被恢复了")
		}()
		if recoverHighestPanicAtFirst {
			//  恢复恐慌1
			defer fmt.Println("恐慌", recover(), "被恢复了")
		}
		defer fmt.Println("现在有两个恐慌共存")
		panic(2)
	}()
	panic(1)
}

func main() {
	demo(true)
	demo(false)
}
The output:
====================
现在有两个恐慌共存
恐慌 1 被恢复了
恐慌 2 被恢复了
====================
现在有两个恐慌共存
恐慌 2 被恢复了
恐慌 1 被恢复了

那么,一个recover调用发挥作用的基本规则是什么呢?

基本规则很简单:
在一个协程中,假设一个recover调用的调用者函数是f并且此f调用的深度为d, 则此recover调用只有在此f调用是一个延迟调用并且有一个深度为d-1的恐慌存在的情况下才会发挥作用。 此recover调用本身是否是延迟的无关紧要。

我认为此规则描述比Go白皮书中的描述更准确。现在,你可以回过头重新看看为什么第一个例子中的标号为7的recover调用没有发挥作用。

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏