22 the Panic Function, the Recover Function and the Defer Statement Part 2

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.