recover
Function
Panic and recover mechanism has been
introduced before,
and several panic/recover use cases are shown in
the last article.
We know that a recover
call can only take effect
if it is invoked in a deferred function call.
However, not all recover
calls in deferred function calls
can catch alive panics.
The remaining of this article will show some useless recover
function calls
and explain what are the right places to call the recover
function.
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())
}()
}
Run it, we will find that
none of the 8 recover
calls in above program recover the program.
The program crashes with printing out the following panic stack traces.
1: <nil>
2: <nil>
3: <nil>
4: <nil>
5: <nil>
6: <nil>
7: <nil>
panic: 789
goroutine 1 [running]:
...
Obviously, the 0th recover
call (the last one) is not reachable.
For others, let's check the rules specified
in Go specification firstly.
recover
is nil
if any of the following conditions holds:
Let's ignore the first condition. The second condition covers the 1st/2nd/3rd and 4th recover calls. The third one covers the 5th and 6th recover calls. However, none of the three conditions cover the 7th recover call.
We know the followingrecover
call will catch the panic.
// example2.go
package main
import (
"fmt"
)
func main() {
defer func() {
fmt.Println( recover() ) // 1
}()
panic(1)
}
But, what is differences between it and the above recover
calls?
What is the principle rule to make a recover
call take effect?
Firstly, let's learn some concepts and facts.
Each function call associated to a call depth, which is related the entry function call of the current goroutine. For the main goroutine, the call depth of a function call is related to the program main entry function call. For a goroutine other than the main goroutine, the call depth of a function call is related to the starting function call of the goroutine.
For example:package main
func main() { // depth 0
go func() { // depth 0
func() { // depth 1
}()
defer func() { // depth 1
defer func() { // depth 2
}()
}()
}()
func () { // depth 1
func() { // depth 2
go func() { // depth 0
}()
}()
go func() { // depth 0
}()
}()
}
The depth of the call to the function containing the current execution point of a goroutine is called the execution depth of the goroutine.
The depth of a panic is the function call depth the panic has propagated to. Please read the next section for a better understanding.
package main
import "fmt"
var o = fmt.Println
func main() { // call depth 0
defer func() { // call depth 1
o("Now, the panic is still in call depth 0")
func() { // call depth 2
o("Now, the panic is still in call depth 0")
func() { // call depth 3
o("Now, the panic is still in call depth 0")
}()
}()
}()
defer o("Now, the panic is in call depth 0")
func() { // call depth 1
defer o("Now, the panic is in call depth 1")
func() { // call depth 2
defer o("Now, the panic is in call depth 2")
func() { // call depth 3
defer o("Now, the panic is in call depth 3")
panic(1)
}()
}()
}()
}
So the depth of a panic is monotonically decreasing, it never increases. And, the depth of an alive panic will never be larger than the execution depth of the goroutine.
package main
import "fmt"
func main() {
defer fmt.Println("program will not crash")
defer func() {
fmt.Println( recover() ) // 3
}()
defer fmt.Println("now, panic 3 suppresses panic 2")
defer panic(3)
defer fmt.Println("now, panic 2 suppresses panic 1")
defer panic(2)
panic(1)
}
Outputs:
now, panic 2 suppresses panic 1
now, panic 3 suppresses panic 2
3
program will not crash
In this example, panic 1 is suppressed by panic 2, and then panic 2 is suppressed by panic 3. So, in the end, there is only one active panic, which is panic 3. Panic 3 is recovered, hence the program will not crash.
In one goroutine, there will be at most one active panic at the same time at the same call depth. In particular, when the execution point runs at the call depth 0 of a goroutine, there will be at most one active panic in the goroutine.
package main
import "fmt"
func main() { // call depth 0
defer fmt.Println("to crash, for panic 3 is still active")
defer func() { // call depth 1
defer func() { // call depth 2
fmt.Println( recover() ) // 6
}()
// The depth of panic 3 is 0,
// and the depth of panic 6 is 1.
defer fmt.Println("now, two active panics: 3 and 6")
defer panic(6) // will suppress panic 5
defer panic(5) // will suppress panic 4
// The following panic will not suppress
// panic 3, for they have different depths.
// The depth of panic 3 is 0.
// The depth of panic 4 is 1.
panic(4)
}()
defer fmt.Println("now, only panic 3 is active")
defer panic(3) // will suppress panic 2
defer panic(2) // will suppress panic 1
panic(1)
}
In this example, panic 6, one of the two active panics, is recovered. But the other active panic, panic 3, is still active at the end of main call, so the program will crash.
The output:now, only panic 3 is active
now, there are two active panics: 3 and 6
6
program will crash, for panic 3 is still active
panic: 1
panic: 2
panic: 3
goroutine 1 [running]
...
(I'm sorry that the code in this section is not correct. It contains a stupid mistake. It is unable to prove the conclusion in the section title. Two panics never coexist in the lifetime of the following program. This section will be removed after some days.)
Example:package main
import "fmt"
func demo(recoverShallowerPanicAtFirst bool) {
fmt.Println("====================")
defer func() {
if !recoverShallowerPanicAtFirst{
// recover panic 1
defer fmt.Println("panic", recover(), "is recovered")
}
defer func() {
// recover panic 2
fmt.Println("panic", recover(), "is recovered")
}()
if recoverShallowerPanicAtFirst {
// recover panic 1
defer fmt.Println("panic", recover(), "is recovered")
}
defer fmt.Println("now, two active panics coexist")
panic(2)
}()
panic(1)
}
func main() {
demo(true)
demo(false)
}
The output:
====================
now, two active panics coexist
panic 1 is recovered
panic 2 is recovered
====================
now, two active panics coexist
panic 2 is recovered
panic 1 is recovered
recover
Call Take Effect?recover
call is f
and the depth of the f
call is d
,
then, to make the recover
call take effect,
the f
call must be a deferred call and
there must be an active panic at depth d-1
.
Whether or not the recover
call itself is deferred is not important.
That is it. I think It is described better than Go specification.
Now you can page up to check why the 7th recover
call in the first example
doesn't take effect, and some others do in some other examples.