Go's defer experience





0 overview

What is defer in Golang? Generally speaking, it is a delayed call. Defer will execute the function registered by defer before the current function returns. For example, defer func_x() will let you register a function variable in the global linked list of defer and call it before the function where the defer statement is exited.

After using Golang for a period of time, the author's understanding of Golang defer has two effects:

  • The panic scene will still be called: this is an important feature, which usually simplifies our code to ensure that no matter what the scene, the defer function must be called, usually used in lock or resource release scenarios;

  • The two supporting behavior codes can be placed in the nearest location: create & release, lock & release, pre & post, making the code easier to read and excellent programming experience. Where is the nearest place? Next line




1 The characteristics of defer

1.1 Delayed call

package main

func main() {
    
    
	defer println("--- defer ---")
	println("--- end ---")
}

Program output:

--- end ---
--- defer ---

Defer will be called before the return of the main function. Core points:

  • Delayed call : Although the defer statement itself is the first line of main, it is printed later;
  • The defer keyword must be in the function context: defer must be placed inside the function ;

1.2 LIFO

What if there are multiple defer calls in a function? Push-to-stack execution, last in first out .

package main

import (
    "strconv"
)

func main() {
    
    
	for i := 1; i <= 6; i++ {
    
    
		defer println("defer -->" + strconv.Itoa(i))
	}
	println("--- end ---")
}

Push-to-stack execution means that the registered function is called first. As above, we registered the sequential formulas 1, 2, 3, 4, 5, 6, and finally printed "— end —", so the result of execution is naturally reversed. The program outputs:

--- end ---
defer -->6
defer -->5
defer -->4
defer -->3
defer -->2
defer -->1

1.3 Scope

Key points: defer and function binding. Two understandings:

  • Defer is only bound to the specific function where the defer statement is located, and the scope is only in this function.
  • Syntactically speaking, the defer statement must also be inside the function, otherwise a syntax error will be reported.
package main

func main() {
    
    
 func() {
    
    
  defer println("--- defer ---")
 }()
 println("--- ending ---")
}

As above, defer is in an anonymous function. As far as the main function is concerned, the anonymous function fun(){}() is called and returned first, and then println("— ending —") is called, so the program output is naturally:

--- defer ---
--- ending ---

1.4 Abnormal scene

This is a very important feature: if an exception occurs in a function, panic can also be executed .

Golang discourages the exception programming model, but it also leaves the panic-recover exception and the mechanism for catching exceptions. So the defer mechanism is particularly important, and it can even be said to be indispensable. Because you don't have a defer mechanism that ignores exceptions and always calls, it is very likely that various resource leaks, deadlocks and other scenarios will occur. why? Because panic occurs, it does not mean that the process will definitely hang, and it is likely to be recovered by the outer layer.

package main

func main() {
    
    
	defer func() {
    
    
		if e := recover(); e != nil {
    
    
			println("--- defer ---")
		}
	}()
 	panic("throw panic")
}

As above, the main function registers a defer and then actively triggers the panic. When the main function exits, the anonymous function registered by defer is called. One more thing, there are actually two main points here:

  • Defer can also be called in abnormal panic scenarios;
  • Recover must be combined with defer to make sense;




2 Usage scenarios

2.1 Concurrent synchronization

The following example performs synchronization control and normal operation on two concurrent coroutines.

var wg sync.WaitGroup

for i := 0; i < 2; i++ {
    
    
    wg.Add(1)
    go func() {
    
    
        defer wg.Done()
        // 程序逻辑
    }()
}
wg.Wait()

2.2 Lock scene

Locking and unlocking must be matched. After Golang has defer, you can write lock and immediately write unlock, so that you will never forget it.

 mu.RLock()
 defer mu.RUnlock()

But please note that the code below the lock will be in the lock until the end of the function . So the following code should be concise and fast enough. If the following logic is very complicated, then you may need to manually control the location of unlock prevention.

2.3 Resource release

Some resources are created temporarily, and the scope only exists in the live function, and needs to be destroyed after being used up. This scenario also applies defer to release. Release the next line just created. This is a very good programming experience. This programming method can greatly avoid resource leakage. Because you can write and release immediately after you write it, you will never forget it again.

// new 一个客户端 client;
cli, err := clientv3.New(clientv3.Config{
    
    Endpoints: endpoints})
if err != nil {
    
    
	log.Fatal(err)
}
// 释放该 client ,也就是说该 client 的声明周期就只在该函数中;
defer cli.Close()

panic-recover exception handling

Recover must be combined with defer. The posture is generally as follows:

 defer func() {
    
    
	  if v := recover(); v != nil {
    
    
	   _ = fmt.Errorf("PANIC=%v", v)
	  }
 }()




3 About defer's pitfall

3.1 Value copy problem when defer is pushed onto the stack

Explanation:
1. When the defer is executed, it will not be executed temporarily, and the statement after defer will be pushed into a separate stack (defer stack).
2. After the function is executed, from the defer stack, follow the first-in-last-out method
Pop the stack, execute 3.defer to push the statement to the stack, it will also copy the value related to the statement, and push it to the stack.
Note: This operation is a copy of the value, not a reference

The test code is as follows:

func sum(n1 int, n2 int) int {
    
    
	// defer 会在该函数执行完成退出时执行
	defer fmt.Println("defer n1=", n1) // n1 = 10
	defer fmt.Println("defer n2=", n2) // n2 = 20

	n1++           // n1 = 11
	n2++           // n2 = 21
	res := n1 + n2 // res = 32
	fmt.Println("sum n1=", n1)
	fmt.Println("sum n2=", n2)
	return res
}

func main() {
    
    
	_ = sum(10, 20)
}

Program output:

sum n1= 11
sum n2= 21
defer n2= 20
defer n1= 10

Experience: I
have written a program before, and after the function is executed, push the message in the function pushed by defer on the stack:

func(){
    
    
	// 定义 alarmInfoMap, alarmStg, alarmClassMap ....
	defer exitRoutine(alarmInfoMap, alarmStg, alarmClassMap)
	// 给 alarmInfoMap, alarmStg, alarmClassMap 赋值等操作 ....
}

The pass parameter of the defer function is to push the message body, and the result message is always empty. After debugging, it is found that there is no message before defer is executed, and the message processing is after the defer function.
The solution is to pass in the reference of the address when passing parameters. The example is as follows:

//退出协程时执行
defer exitRoutine(&alarmInfoMap, &alarmStg, &alarmClassMap)

If you want to learn more Go language grammar and common knowledge points of Go language at work, you can refer to my note source code https://github.com/qiuyunzhao/go_basis

Guess you like

Origin blog.csdn.net/QiuHaoqian/article/details/106234130