Article directory
- `defer` and exception handling
-
- Why use `defer`
- How to use `defer`
- Notes on using `defer`
-
- Precomputed parameters
- What are exceptions and errors
- How to handle exceptions and errors
- Considerations for handling exceptions and errors
defer
and exception handling
In this article, we introduce the use and precautions of defer
in Go language, and how to use defer
to handle exceptions in Go language. defer
is a keyword provided by the Go language. Its function is that when you register a defer
statement, the statement will be returned when the function returns strong>is executed.
Why use defer
In C\C++
or other languages that require manual management of certain resources (including but not limited to memory, file descriptors, etc.), we may have the following situations, that is, we have opened A new piece of memory, or a new file descriptor is opened. We may remember to release it at the beginning, but as we write the code, we may forget about it by the end. In this way, it may This leads to resource leakage, so various languages provide one or more mechanisms to release resources at the end of the code block, the end of the function call, and the end of the variable life cycle. For example, C++ provides RAII to use the destructor to release resources after the current object declaration cycle ends. Rust provides a life cycle method to ensure that resources are released correctly, and defer
is provided by the Go language. Method used to release resources.
defer
In Go language, it can be used to release mutex locks, increase the count of waitGroup
, roll back database transactions, and handle various function calls Epilogue
. Although it is manual, it is very powerful.
How to use defer
The basic usage of defer
is as follows:
func deferExample1() {<!-- --> defer fmt.Println("deferExample1's defer Invoke!") fmt.Println("deferExample1 Invoke!") }
You can see that the implementation executes other codes inside the function, and the defer
statement is executed last. The defer
keyword can be followed not only by a statement, but also by a function call. Statement, examples are as follows:
func deferExample2() {<!-- --> defer func() {<!-- --> fmt.Println("deferExample2's defer function Invoke!") }() fmt.Println("deferExample2 Invoke!") }
And you can use the defer
keyword to register multiple function call statements or ordinary statements in a function. Examples are as follows:
func deferExample3() {<!-- --> defer fmt.Println("deferExample3's defer statement 1 execute!") defer func() {<!-- --> fmt.Println("deferExample3's defer function 2 execute!") }() defer func() {<!-- --> fmt.Println("deferExample3's defer function 3 execute!") }() defer fmt.Println("deferExample3's defer statement 4 execute!") fmt.Println("deferExample3 Invoke!") }
Here we can see that the execution order of the defer
registration statement is opposite to the registration order. We register in the order of 1,2,3,4, while the call is in the order of 4,3,2,1 called in sequence. This is because the runtime of the Go language maintains a “chain stack” during function execution. Each time defer
is called, a new item will be added to this “chain stack”, and we know that the stack This data structure has the characteristics of FILO (First In Last Out), so the first registered defer
statement will be called last. The specific process is as follows:
This picture clearly explains the entire workflow of defer
. You can see how the defer
statement is registered and executed. And the key point is that derfer
is not executed after the function returns, but it is executed during the function return because the return of the function is not an atom. Operation, there are many things to be done during the return period, such as when processing the function stack frame, it is necessary to release local variables, assign values to the return value register, etc., so the defer
statement is also completed during the return period.
Notes on using defer
Regarding the defer
keyword during the interview, in addition to the above-mentioned execution process, you will usually be given a piece of code to tell you the execution result of the code, so we will also take a look at the defer
keyword. The details of code>defer usage.
Precomputed parameters
When using defer
, this is a common scenario, which is to use a variable of the current function in defer
. Let’s take an example to understand:
func deferExample4() {<!-- --> i := 5 defer fmt.Println("deferExample4's defer i = ", i) i=10 fmt.Println("deferExample4's i = ", i) }
We can see that the value printed inside the function here is 10, and the value printed in the defer
statement is 5, because the defer
statement has the function of precomputing parameters, which means that we are When registering the defer
statement, the value in the defer
statement has already been calculated, so the i in the
defer
statement code> is assigned when registering.
This situation is consistent with when we use defer
to make a function call and pass the variables inside the function body as parameters to the function registered with defer
. The example is as follows:
func deferExample5() {<!-- --> i := 5 defer func(i int) {<!-- --> fmt.Println("deferExample5's defer i = ", i) }(i) i=10 fmt.Println("deferExample5's i = ", i) }
But when using a closure to capture the variables in the function body into the defer
registered function, the situation will change. The example is as follows:
func deferExample6() {<!-- --> i := 5 defer func() {<!-- --> fmt.Println("deferExample6's defer i = ", i) }() i=10 fmt.Println("deferExample6's i = ", i) }
Here we get the same output, this is because the defer
function captures the variable inside the function when it is registered, and the defer
statement is executed during the return of the function , here i
inside the defer function
and i
inside the function point to the same variable address, so defer function
will print the same value as the function.
However, there is a special case here, that is, when the return value is a named return value, it will have different results. The example is as follows:
func deferExample7() (i int) {<!-- --> i = 5 defer func() {<!-- --> fmt.Printf("deferExample7's defer i = %d,addr = %p\\ ", i, & amp;i) }() i=10 fmt.Printf(""deferExample7's i = %d,addr = %p\\ ", i, & amp;i) i = 3 return i + 2 }
If we follow the above closure rules, then the expected result should be 3, because 3 is assigned to i at the end, but since this is a named return parameter, and the defer
statement is returning It was executed during the period, so we got 5. Use the following picture to explain this phenomenon:
In this way, we clearly know why using a closure to capture a named return value has such a different effect. This is because the defer
statement is executed during the return of the function, and defer
The statement captures the named return value i and obtains the address of i when registering, so the function assigns the return value to the named return value when it returns. At this time, the value of the named return value i occurs. Change, and after assigning the return value, the cleanup work of the function call begins, such as releasing the stack frame and executing the defer
statement, so the named return value used in the defer
statement is The latest value is 5.
Another point is that when we use defer
, we cannot just define an anonymous function after the defer
keyword without calling it. This is wrong.
To sum up, we should pay attention to the following points when using defer
:
-
The
defer
keyword can only be followed by a statement, not by the definition of an anonymous function! -
The
defer
statement is executed during the return process of the function, not after the function returns. -
The
defer
statement has the function of pre-calculating values. When we do not use a closure to capture the variables inside the function, thedefer
statement will calculate the value to be used when registering. -
The
defer
function closure captures the variable inside the function. When thedefer
function is executed, the latest value of the variable will be obtained, because thedefer
function obtains The address of this variable. -
defer
When a function closure captures a named return value, if the return value is explicitly specified inside the function, then this return value will be the latest value of the named return value, thedefer
function will also use the latest value when executing.
What are exceptions and errors
There are certain differences between exceptions and errors in Go language and exceptions and errors in other languages, so we mainly want to understand what exceptions and errors are in Go language. Simply put, exceptions in Go language are when the program is running. An exception is thrown by panic
. This exception will cause the program to crash if it is not recovered. The error in Go language is the type that implements the error
interface. We can use it in the function The return value returns a value that implements the error
interface, which is used to tell the caller of the function whether the current function is executed normally, and if not, what is the error message.
How to handle exceptions and errors
In the Go language, we generally use the following two methods to handle exceptions and errors.
For errors, we generally handle them like this:
Ahem, seriously speaking, because error in the Go language is an interface, the error returned is always a value. We can judge this value to understand what error is currently occurring and how to handle the error. For example:
if err == ErrSomething {<!-- --> … }
Here, if the current error is ErrorSomething
, we will handle the error. If it is other errors, the same is true. But what we most commonly use is the code in the picture above (every Gopher needs such a keyboard!), because we generally do not handle errors that occur in the current function in the current function. We usually propagate the error back to the upper layer. Decide what to do with it.
As for exceptions, which are caused by panic
, exceptions will generally cause the program to crash, which means that a very serious error has occurred. If we are in the development environment, we generally will not deal with the error, but Use this error to debug. If it is an online running environment and we are worried about a panic, then we usually use the recover
function to capture this panic
and treat it as a The error is passed out. The specific example is as follows:
package main import "fmt" func mayPanic(){<!-- --> panic("a problem") } func main(){<!-- --> defer func(){<!-- --> if err:=recover();err!=nil{<!-- --> fmt.Println(err) } }() mayPanic() }
In this code, we use recover
to capture the panic
that occurs and then print it out. In an actual environment, we can also use this method to prevent program crashes.
In the Go language, exceptions and errors are generally handled in the above two ways.
Notes on handling exceptions and errors
In addition to the above methods, there are some special situations that need to be understood. Next, let’s look at a special example, that is, a panic
event occurs in the function executed by defer
how is it? Examples are as follows:
func mayPanic() {<!-- --> defer func() {<!-- --> fmt.Println("may Panic 1") }() defer func() {<!-- --> panic("I'll panic!") }() defer func() {<!-- --> fmt.Println("may Panic 2") }() fmt.Println("mayPanic Invoke") } func main() {<!-- --> defer func() {<!-- --> fmt.Println("main function defer invoke") }() mayPanic() fmt.Println("main function invoke!") }
For this code, the results of running are as follows:
Let’s look at another example for comparison:
func mayPanic() {<!-- --> defer func() {<!-- --> fmt.Println("may Panic 1") }() defer func() {<!-- --> panic("I'll panic!") }() defer func() {<!-- --> fmt.Println("may Panic 2") }() panic("It's problem") defer func(){<!-- --> fmt.Println("may Panic 3") }() } func main() {<!-- --> defer func() {<!-- --> fmt.Println("main function defer invoke") }() mayPanic() fmt.Println("main function invoke!") }
The output of this example is as follows:
From the above two examples, we can simply draw a conclusion: When panic
occurs inside a function, if before panic
occurs, If the defer
statement is registered, then panic
will be postponed until all defer
statements are executed. If multiple defer
statements are registered and one or more of the multiple defer
statements panic
occurs, it will not affect the remaining When the defer
statement is executed, only the panic
information will be recorded until all defer
statements are executed, and then the >panic
returns to the upper-layer function together. If the upper-layer function does not capture it, then the panic
will cause the program to crash.
As for why this happens, of course it is because we need to capture panic
in the defer
statement, and the compiler does not know which defer
Only statements can be captured and can only be executed in full. If there is no defer
statement registered in the current function, then the function will directly propagate panic
to the upper layer.
As for the panic
that occurs in the defer
statement, if you need to capture it, then do it in the defer
statement. The example is as follows:
func mayPanic() {<!-- --> defer func() {<!-- --> fmt.Println("may Panic 1") }() defer func() {<!-- --> defer func(){<!-- --> if err:=recover();err!=nil{<!-- --> fmt.Println(err) } }() panic("I'll panic!") }() defer func() {<!-- --> fmt.Println("may Panic 2") }() panic("It's problem") defer func(){<!-- --> fmt.Println("may Panic 3") }() }
That’s all about panic
for now. Next, let’s look at the content about error
. We mainly understand this part, except:
if err!=nil{<!-- --> return nil,err }
Other processing methods, such as custom errors, as we said earlier, in the Go language, as long as a type implements the error
interface, then the type can be used as an error, for example:
type MyError struct{<!-- --> name string age int } func (m *MyError) Error() string {<!-- --> return m.name + string(m.age) }
In this way, we have customized an error. When we need to use it, we can treat this type as an error.