22 The panic Function, the recover Function and the defer Statement - Part 2 #
Hello, I’m Haolin, and today we will continue discussing the panic function, the recover function, and the defer statement.
In my previous article, I mentioned that a panic can contain a value, which briefly explains the reason for the panic.
If a panic is unintentionally caused by us, the value inside can only be given by the Go runtime system. However, when we intentionally use the panic
function to cause a panic, we can specify the value it contains. Our first question today is about this latter situation.
Knowledge Expansion #
Question 1: How do you pass a value to panic
, and what kind of value should it be?
#
It’s actually quite simple. When calling the panic
function, you can pass a value as an argument to the function. Since the only parameter of the panic
function is of type interface{}
, which is an empty interface, technically, it can accept values of any type.
However, it is best to pass an error value of type error
or any other value that can be effectively serialized. By “effectively serialized”, I mean a representation that is more readable and can be transformed.
Remember? For the various printing functions in the fmt
package, the Error
method of the error
type is equivalent to the String
method of other types. Both methods produce a result of type string
.
When printing these values using the %s
placeholder, their string representation is generated using either of these two methods.
Once a program encounters an exception, it is important to record relevant information about the exception, usually in the program logs.
When troubleshooting errors in a program, the first thing to do is to check and interpret the program logs. The most commonly used and convenient way to log the relevant values is to log their string representations.
So, if you think a value may be logged, it should be associated with a String
method. If the value is of type error
, then its Error
method should return a customized string representation for it.
For this purpose, you might think of using fmt.Sprintf
or fmt.Fprintf
-type functions that can format and output arguments.
Yes, they can be used to output some kind of representation of values. However, they are definitely not as powerful as the Error
method or the String
method we define ourselves. Therefore, it is always preferred to write these two methods separately for different data types.
But what does all of this have to do with the value passed to the panic
function? Actually, the principle is the same. At least when the program crashes, the string representation of the value contained in the panic will be printed out.
In addition, we can take certain protective measures to prevent the program from crashing. At this time, the value contained in the panic will be extracted and usually printed out or logged after extraction.
Since we have mentioned protective measures against panic, let’s take a look at another question.
Question 2: How do you take protective measures against panic to prevent a program from crashing? #
The built-in recover
function in Go is used to recover from a panic or, in other words, to quell a runtime panic. The recover
function does not require any parameters and returns a value of type empty interface.
If used correctly, this value is actually the value contained in the panic that is about to be recovered. And if this panic is caused by our call to the panic
function, then this value will also be a copy of the parameter value we passed in when calling the panic
function. Please note that emphasis is given to the correct usage here. Let’s see what is an incorrect usage.
package main
import (
"fmt"
"errors"
)
func main() {
fmt.Println("Enter function main.")
// Trigger a panic.
panic(errors.New("something wrong"))
p := recover()
fmt.Printf("panic: %s\n", p)
fmt.Println("Exit function main.")
}
In the main
function above, I first trigger a panic by calling the panic
function and then try to recover this panic by calling the recover
function. So what happens? Give it a try and you will find that the program still crashes, and the recover
function call has no effect, not even a chance to execute.
Remember? I mentioned that once a panic occurs, control quickly propagates up the call stack in the opposite direction. Therefore, there is no chance for the code after the panic
function call to be executed.
So what if I move the code that calls the recover
function forward? In other words, what if I call the recover
function first, and then the panic
function?
Obviously, this won’t work either because if no panic occurs when we call the recover
function, the function won’t do anything and will simply return nil
.
In other words, it is meaningless to do this. So what is the correct usage of the recover
function? This brings us to the defer
statement.
As the name suggests, the defer
statement is used to defer the execution of code. It is deferred until the moment just before the function where the statement is located is about to complete execution, regardless of the reason for completing execution.
This is similar to the go
statement. A defer
statement always consists of a defer
keyword and a call expression.
There are some restrictions here. Some call expressions cannot appear here, including call expressions for built-in functions in Go and functions in the unsafe
package.
By the way, these restrictions also apply to call expressions in go
statements. In addition, the function being called here can have a name or be anonymous. We can call the function here a deferred function. Note that it is the deferred function, not the defer
statement, that is deferred.
I just said that no matter what the reason for the end of the function execution, the defer
function calls inside it will be executed at the moment it is about to end. This is true even if the reason for the end of the execution is a panic. Because of this, we need to use the defer
statement and the recover
function call together to recover from a panic that has already occurred.
Let’s take a look at the corrected code.
package main
import (
"fmt"
"errors"
)
func main() {
fmt.Println("Enter function main.")
defer func(){
fmt.Println("Enter defer function.")
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// Cause a panic.
panic(errors.New("something wrong"))
fmt.Println("Exit function main.")
}
In this main
function, I first wrote a defer
statement and called the recover
function inside the defer
function. Only when the result value of the call is not nil
, i.e. only when a panic has actually occurred, will I print a line prefixed with “panic:”.
Next, I called the panic
function and passed in an error
value. Here, it is important to note that we should try to write the defer
statement at the beginning of the function body because all statements after the statement that causes the panic will not have a chance to execute.
Only in this way, the recover
function call inside the defer
function will intercept and recover from any panics that occur in the function belonging to the defer
statement, as well as in the code it calls.
So far, I have shown you two typical incorrect uses of the recover
function, as well as a basic correct use.
I hope you can remember the reason behind the incorrect uses and truly understand the essence of using the defer
statement and the recover
function call together.
In the command source code file demo50.go
, I have combined the above three usages into a single piece of code. You can run that file and experience the different effects of each usage.
Now let me tell you more about the defer
statement.
Question 3: If a function has multiple defer
statements, what is the execution order of the defer
function calls?
#
In short, in the same function, the execution order of defer
function calls is completely opposite to the order in which the defer
statements they belong to appear (or more precisely, the execution order).
When a function is about to end its execution, the defer
function call at the bottom will be executed first, followed by the defer
function call closest to it and above it, and so on, and the defer
function call at the top will be executed last.
If there is a for
statement in the function and the for
statement contains a defer
statement, then obviously the number of times the defer
statement is executed depends on the number of iterations of the for
statement.
And each defer
function call in the same defer
statement will be generated and not immediately executed every time it is executed.
Now the question is, in the for
statement, in what order will the multiple defer
function calls generated by this for
statement be executed?
To thoroughly understand this, we need to understand what happens when the defer
statement is executed.
It’s actually not complicated. When the defer
statement is executed, the Go language will store the defer
function and its parameter values separately in a linked list.
This linked list corresponds to the function to which the defer
statement belongs, and it is a First-In-Last-Out (FILO) structure, similar to a stack.
When it is necessary to execute the defer
function calls in a function, the Go language will first get the corresponding linked list, and then take out the defer
function and its parameter values from the linked list one by one and execute the calls.
This is why I said that the execution order of defer
function calls is completely opposite to the order in which they belong to the defer
statements.
Now it’s your turn. I wrote a sample code related to this question in the demo51.go
file, and the core code in it is very simple, only a few lines.
I want you to first look at the code, then think about and write down what will be printed when the sample is run.
If you can’t figure it out, you can run the sample first and then try to explain the printed content. In any case, you need to fully understand the exact reason why those few lines of content will appear in that order.
Summary #
In our last two sections, we mainly talked about two functions and one statement. The recover
function is specifically used to recover from panics, and it is called to trigger the recovery process.
When called, it returns an empty interface{} value. If no panic occurred when calling it, the result value will be nil
.
However, if the panic being recovered is the one triggered by calling the panic
function, the result value returned will be a copy of the argument value passed to the panic
function.
The call to the recover
function only takes effect within a defer
statement. The defer
statement is used to delay the execution of code.
More precisely, it will delay the execution of the defer
function until the moment when the function to which the defer
statement belongs is about to finish execution.
Within the same function, the execution order of the delayed defer
function calls will be completely opposite to the order in which the defer
statements belong. It is also important to note that for each execution of the same defer
statement, a delayed defer
function call will be generated.
This situation often occurs when the defer
statement is used in conjunction with a for
statement. In this case, it is important to pay attention to the actual execution order of multiple defer
function calls generated by the same defer
statement within the for
statement.
The above describes the core knowledge about special program exceptions in Go and their handling methods. This can give rise to many interview questions.
Thought Questions #
Can we raise a panic within the defer
function, while recovering from a panic?
Click here to view the accompanying code for the Go Language column article.