golang的panic和recover

What is Panic?

The idiomatic way of handling abnormal conditions in a Go program is using errors. Errors are sufficient for most of the abnormal conditions arising in the program.

But there are some situations where the program cannot continue execution after an abnormal condition. In this case, we use panic to prematurely terminate the program. When a function encounters a panic, its execution is stopped, any deferred functions are executed and then the control returns to its caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates. This concept will be more clear when we write an example program.

It is possible to regain control of a panicking program using recover which we will discuss later in this tutorial.

panic and recover can be considered similar to try-catch-finally idiom in other languages such as Java except that they are rarely used in Go.

When Should Panic Be Used?

One important factor is that you should avoid panic and recover and use errors where ever possible. Only in cases where the program just cannot continue execution should panic and recover mechanism be used.

There are two valid use cases for panic.

  • An unrecoverable error where the program cannot simply continue its execution.
    One example is a web server that fails to bind to the required port. In this case, it’s reasonable to panic as there is nothing else to do if the port binding itself fails.

  • A programmer error.
    Let’s say we have a method that accepts a pointer as a parameter and someone calls this method using a nil argument. In this case, we can panic as it’s a programmer error to call a method with nil argument which was expecting a valid pointer.

Panic Example

The signature of the built-in panic function is provided below,

1
func panic(interface{})  

The argument passed to the panic function will be printed when the program terminates. The use of this will be clear when we write a sample program. So let’s do that right away.

We will start with a contrived example which shows how panic works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func fullName(firstName *string, lastName *string) {
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

The above is a simple program to print the full name of a person. The fullName function in line no. 7 prints the full name of a person. This function checks whether the firstName and lastName pointers are nil in line nos. 8 and 11 respectively. If it is nil the function calls panic with a corresponding message. This message will be printed when the program terminates.

Running this program will print the following output,

1
2
3
4
5
6
7
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193
main.main()
/tmp/sandbox210590465/prog.go:20 +0x4d

Let’s analyze this output to understand how panic works and how the stack trace is printed when the program panics.

In line no. 19 we assign Elon to firstName. We call fullName function with lastName as nil in line no. 20. Hence the condition in line no. 11 will be satisfied and the program will panic. When panic is encountered, the program execution terminates, the argument passed to the panic function is printed followed by the stack trace. Since the program terminates following the panic function call in line no. 12, the code in line nos. 13, 14, and 15 will not be executed.

This program first prints the message passed to the panic function,

1
panic: runtime error: last name cannot be nil  

and then prints the stack trace.

The program panicked in line no. 12 of fullName function and hence,

1
2
3
goroutine 1 [running]:  
main.fullName(0xc00006af58, 0x0)
/tmp/sandbox210590465/prog.go:12 +0x193

will be printed first. Then the next item in the stack will be printed. In our case, line no. 20 where the fullName is called is the next item in the stack trace. Hence it is printed next.

1
2
main.main()  
/tmp/sandbox210590465/prog.go:20 +0x4d

Now we have reached the top level function which caused the panic and there are no more levels above, hence there is nothing more to print.

One More Example

Panics can also be caused by errors that happen during the runtime such as trying to access an index that is not present in a slice.

Let’s write a contrived example which creates a panic due to out of bounds slice access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func slicePanic() {
n := []int{5, 7, 4}
fmt.Println(n[4])
fmt.Println("normally returned from a")
}
func main() {
slicePanic()
fmt.Println("normally returned from main")
}

In the program above, in line no. 9 we are trying to access n[4] which is an invalid index in the slice. This program will panic with the following output,

1
2
3
4
5
6
7
panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.slicePanic()
/tmp/sandbox942516049/prog.go:9 +0x1d
main.main()
/tmp/sandbox942516049/prog.go:13 +0x22

Defer Calls During a Panic

Let’s recollect what panic does. When a function encounters a panic, its execution is stopped, any deferred functions are executed and then the control returns to its caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates.

In the example above, we did not defer any function calls. If a deferred function call is present, it is executed and then the control returns to its caller.

Let’s modify the example above a little and use a defer statement.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

func fullName(firstName *string, lastName *string) {
defer fmt.Println("deferred call in fullName")
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

The only changes made are the addition of the deferred function calls in line nos. 8 and 20.

This program prints,

1
2
3
4
5
6
7
8
9
deferred call in fullName  
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af28, 0x0)
/tmp/sandbox451943841/prog.go:13 +0x23f
main.main()
/tmp/sandbox451943841/prog.go:22 +0xc6

When the program panics in line no. 13, any deferred function calls are first executed and then the control returns to the caller whose deferred calls are executed and so on until the top level caller is reached.

In our case, defer statement in line no. 8 of fullName function is executed first. This prints the following message.

1
deferred call in fullName  

And then the control returns to the main function whose deferred calls are executed and hence this prints,

1
deferred call in main  

Now the control has reached the top level function and hence the program prints the panic message followed by the stack trace and then terminates.

Recovering from a Panic

recover is a builtin function that is used to regain control of a panicking program.

The signature of recover function is provided below,

1
func recover() interface{}  

Recover is useful only when called inside deferred functions. Executing a call to recover inside a deferred function stops the panicking sequence by restoring normal execution and retrieves the error message passed to the panic function call. If recover is called outside the deferred function, it will not stop a panicking sequence.

Let’s modify our program and use recover to restore normal execution after a panic.

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
package main

import (
"fmt"
)

func recoverFullName() {
if r := recover(); r!= nil {
fmt.Println("recovered from ", r)
}
}

func fullName(firstName *string, lastName *string) {
defer recoverFullName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

The recoverFullName() function in line no. 7 calls recover() which returns the value passed to panic function call. Here we are just printing the value returned by recover in line no. 9. recoverFullName() is being deferred in line no. 14 of the fullName function.

When fullName panics, the deferred function recoverName() will be called which uses recover() to stop the panicking sequence.

This program will print,

1
2
3
recovered from  runtime error: last name cannot be nil  
returned normally from main
deferred call in main

When the program panics in line no. 19, the deferred recoverFullName function is called which in turn calls recover() to regain control of the panicking sequence. The call to recover() in line no. 8 returns the argument passed to panic() and hence it prints,

1
recovered from  runtime error: last name cannot be nil  

After execution of recover(), the panicking stops and the control returns to the caller, in this case, the main function. The program continues to execute normally from line 29 in main since the panic has been recovered 😃. It prints returned normally from main followed by deferred call in main

Let’s look at one more example where we recover from a panic caused by accessing an invalid index of a slice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func recoverInvalidAccess() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}

func invalidSliceAccess() {
defer recoverInvalidAccess()
n := []int{5, 7, 4}
fmt.Println(n[4])
fmt.Println("normally returned from a")
}

func main() {
invalidSliceAccess()
fmt.Println("normally returned from main")
}

Running the above program will output,

1
2
Recovered runtime error: index out of range [4] with length 3  
normally returned from main

From the output, you can understand that we have recovered from the panic.

https://golangbot.com/panic-and-recover/