Golang A clever use of defer for error handling

  • 2020-06-03 06:56:23
  • OfStack

This paper mainly introduces the contents related to Golang's skillful use of defer for error handling, and shares them for your reference and learning. Here is a detailed introduction:

Problem is introduced into

Error handling is undoubtedly an important part of a program, and efficient and elegant handling of errors is the pursuit of most programmers. Many programmers have C/C++ programming backgrounds, and Golang programmers are no exception, and their handling of errors is intentionally or unintentionally branded with C/C++.

Let's look at the following example, there is a deja vu, the code is as follows:


func deferDemo() error {
 err := createResource1()
 if err != nil {
 return ERR_CREATE_RESOURCE1_FAILED
 }
 err = createResource2()
 if err != nil {
 destroyResource1()
 return ERR_CREATE_RESOURCE2_FAILED
 }

 err = createResource3()
 if err != nil {
 destroyResource1()
 destroyResource2()
 return ERR_CREATE_RESOURCE3_FAILED
 }

 err = createResource4()
 if err != nil {
 destroyResource1()
 destroyResource2()
 destroyResource3()
 return ERR_CREATE_RESOURCE4_FAILED
 }
 return nil
}

As you can see from the code implementation: in a function, when the creation of a new resource fails, all previously created resources are cleaned up, which gives the function a bad taste of duplicate code. For example, the destroyResource1 function is called 3 times and destroyResource2 function is called 2 times.

Reconstruct 1:1 defer + multiple flag

Golang provides a nice keyword, defer, that calls defer statements when a function containing defer has been executed (either by the normal end of return or by an exception due to panic).

With this point in mind, we try to clean up all resources in defer statement 1. Because when the function returns, you don't know if you need to clean up and those resources, you add more flag.

The refactored code looks like this:


func deferDemo() error {
 flag := false
 flag1 := false
 flag2 := false
 flag3 := false

 defer func() {
 if !flag {
 if flag3 {
  destroyResource3()
 }
 if flag2 {
  destroyResource2()
 }
 if flag1 {
 destroyResource1()
 }
 }
 }()

 err := createResource1()
 if err != nil {
 return ERR_CREATE_RESOURCE1_FAILED
 }
 flag1 = true

 err = createResource2()
 if err != nil {
 return ERR_CREATE_RESOURCE2_FAILED
 }
 flag2 = true

 err = createResource3()
 if err != nil {
 return ERR_CREATE_RESOURCE3_FAILED
 }
 flag3 = true

 err = createResource4()
 if err != nil {
 return ERR_CREATE_RESOURCE4_FAILED
 }
 flag = true
 return nil
}

As you can see from the refactored code, while eliminating duplication, too much flag was introduced:

flag indicates whether the function has executed successfully, that is, when flag is true, it means the function has executed successfully; otherwise, it means the function has failed. In the defer statement, only when flag is false do you need to clean up resources in unit 1 flagi indicates whether the i resource was created successfully, that is, when flagi is true, it means the i resource was created successfully; otherwise, it means the creation of i resource failed. In the defer statement, the i resource needs to be cleaned up only if flagi is true

Obviously, that's not what we want

Refactoring 2: Multiple defer

Those of you who have read the linux source code know that in the kernel code, many places through the goto statement to focus on handling errors, very elegant.

In this way, we write the pre-refactoring code 1 in C language. The code is as follows:


ErrCode deferDemo()
{
 ErrCode err = createResource1();
 if (err != ERR_SUCC)
 {
 goto err_1;
 }

 err = createResource2();
 if (err != ERR_SUCC)
 {
 goto err_2;
 }

 err = createResource3();
 if (err != ERR_SUCC)
 {
 goto err_3;
 }

 err = createResource4();
 if (err != ERR_SUCC)
 {
 goto err_4;
 }

 return ERR_SUCC;

 err_4:
 destroyResource3();
 err_3:
 destroyResource2();
 err_2:
 destroyResource1();
 err_1:
 return ERR_FAIL;
}

There is no repetition, no flag, error handling is elegant, it feels good, then the previous rule prohibiting the use of goto statement in C/C++ coding specification is a bit too much, hehe...

As can be seen from the reconstructed C code, the order of create operation and destroy operation is similar to the order of push and push:

With create operations, destroy operations are pushed one by one, in order 1,2,3 The destroy operation is out of the stack, in order 3,2,1

This brings us to the defer statement: when the Golang code executes, if it encounters an defer statement, it is pushed onto the stack, and when the function returns, the defer statement is called last in, first out.

Let's look at an example. The code is as follows:


func main() {
 defer fmt.Println(1)
 defer fmt.Println(2)
 defer fmt.Println(3)
}

After running, the log is as follows:


3
2
1

However, having a stack property is not enough, because with create, the destroy push is conditional:

If the create operation fails, the defer statement is not executed, resulting in the destroy operation not being pushed If the create operation is successful, the defer statement is executed and the destroy operation completes the push

It can be seen that the push condition of destroy operation is the success of create operation, but destroy operation is not a fixed execution, only when an create operation fails ("err! = nil"), the previous destory operation needs to be executed, so the value of err also needs to be pushed. However, when destroy pushes, "err == nil", the problem becomes that when the value of err later becomes non-ES127en, the value of err on the stack should be changed synchronously, meaning that a reference or pointer is passed instead of a value.

defer must be followed by a closure call when both the reference or pointer to err and the destroy operation need to be pushed. We know that values are passed for arguments to closures and references are passed for external variables. For simplicity and elegance, err is passed not by a pointer to a parameter, but by a reference to an external variable.

Based on this conclusion, we refactor the 1 code as follows:


func deferDemo() error {
 err := createResource1()
 if err != nil {
 return ERR_CREATE_RESOURCE1_FAILED
 }
 defer func() {
 if err != nil {
 destroyResource1()
 }
 }()

 err = createResource2()
 if err != nil {
 return ERR_CREATE_RESOURCE2_FAILED
 }
 defer func() {
 if err != nil {
 destroyResource2()
 }
 }()

 err = createResource3()
 if err != nil {
 return ERR_CREATE_RESOURCE3_FAILED
 }
 defer func() {
 if err != nil {
 destroyResource3()
 }
 }()

 err = createResource4()
 if err != nil {
 return ERR_CREATE_RESOURCE4_FAILED
 }
 return nil
}

This refactoring eliminates the bad taste of code, can not help sigh 1 sentence: "upgrade, my brother! "

conclusion

This article handles errors efficiently and elegantly by using defer skillfully, a technique that should be mastered and widely used by all Golang programmers.


Related articles: