defer and exception handling

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!")
}

image-20231107220137706

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!")
}

image-20231107220524459

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!")
}

image-20231107220939503

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:

image-20231107222133876

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)
}

image-20231107223215404

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 idefer 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)
}

image-20231107223834078

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)
}

image-20231107224112457

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
}

image-20231107225428455

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:

image-20231107230151521

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.

image-20231109200855177

To sum up, we should pay attention to the following points when using defer:

  • The deferkeyword 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, the defer statement will calculate the value to be used when registering.

  • The defer function closure captures the variable inside the function. When the defer function is executed, the latest value of the variable will be obtained, because the defer function obtains The address of this variable.

  • deferWhen 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, the defer 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:

Copypasting & quot;if err!= nil {return err;} everywhere : r /golang

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:

image-20231109202428036

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:

image-20231109202517472

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.

syntaxbug.com © 2021 All Rights Reserved.