Analysis of go underlying series defer principle

Analysis of go underlying series defer principle

defer

preface

  • defer statement
    • Used to delay the call of a function
    • Every time defer pushes a function onto the stack
    • Before the function returns, the delayed function is taken out and executed
  • We call the function that creates defer the master function
  • The function following the defer statement is called a delay function
    • The delay function may have input parameters
      • Parameters may come from functions that define defer
    • A delay function may also refer to a variable that the main function uses to return
      • That is, the delay function may affect some behavior of the main function
    • In these scenarios, it is easy to make mistakes if you do not understand the rules of defer

warm-up

// What is the output of the following function?
func deferFuncParameter()  {
	var aInt = 1

	defer fmt.Println(aInt)

	aInt = 2
	return
}

func main() {
	deferFuncParameter()
}
// Title Description:
// 		The function deferFuncParameter() defines an integer variable and initializes it to 1,
//		Then use the defer statement to print out the variable value, and finally modify the variable value to 2
// Reference answer:
// 		Output 1.
//		Delay function FMT The parameter of println (aint) has been determined when the defer statement appears (this sentence is the key)
//		Therefore, no matter how to modify aInt variable later, it will not affect the delay function



// What does the following program output?
func printArray(array *[3]int)  {
	for i := range array {
		fmt.Println(array[i])
	}
}

func deferFuncParameter()  {
	var aArray = [3]int{1, 2, 3}

	defer printArray(&aArray)

	aArray[0] = 10
	return
}

func main() {
	deferFuncParameter()
}
// Function Description:
//		The function deferFuncParameter() defines an array and delays the call of the function printArray() through defer,
// 		Finally, modify the first element of the array. The printArray() function takes the pointer to the array and prints out all the arrays
// Reference answer:
// 		Output 10, 2 and 3 values. The parameters of the delay function printArray() have been determined when the defer statement appears, that is, the address of the array
//		Because the execution time of the delay function is before the return statement (this sentence is the key)
//		Therefore, the final modified value of the array will be printed



// What does the following function output?
func deferFuncReturn() (result int) {
	i := 1

	defer func() {
		result ++
	}()

	return i
}

func main() {
	fmt.Println(deferFuncReturn())
}
// Function Description:
// 		The function has a named return value result, and a variable i is declared inside the function,
//		defer specifies a delay function and finally returns the variable i. Increment result in delay function
// Reference answer:
//		Function output 2.
//		The return statement of the function is not atomic,
//		The actual implementation is divided into: set return value - > RET,
//		Before the actual execution of the defer statement, that is, the return process of the function with defer is: set the return value - > execute defer - > ret.
//		Therefore, the return statement first sets the result to the value of i, that is, 1, and the defer statement increments the result by 1, so it finally returns 2

defer rule

Rule 1: the parameters of the delay function are determined when the defer statement appears
func a() {
    i := 0
    defer fmt.Println(i)
    i ++
    return
}

// FMT in defer statement The println() parameter i value is determined when defer appears
// It's actually a copy.
// Subsequent changes to variable i will not affect FMT "0" is still printed after the println() function is executed. ".

// Note: for pointer type parameters, the rule still applies
// 		Only the parameter of the delay function is an address value
// In this case
// 		The modification of variables in the statements after defer may affect the delay function
Rule 2: the delay function is executed in the order of last in first out, that is, the defer that appears first is executed last
  • This rule is easy to understand. Defining defer is similar to the stack operation, and executing defer is similar to the stack operation
  • The original intention of designing defer is to simplify the action of resource cleaning when the function returns
  • Resources often have a dependency order
    • For example, first apply for resource A, then apply for resource B according to resource A, and apply for resource C according to resource B
    • That is, the application sequence is: A - > b - > C
    • Release is often reversed. This is the reason why defer is designed as FIFO
    • It's a good habit to define a defer to release resources immediately when an application runs out of resources to be released.
Rule 3: the delay function may operate on the named return value of the main function
  • The function that defines defer, that is, the main function may have a return value
  • It doesn't matter whether the return value has a name or not. The function of defer, that is, the delay function, may affect the return value.
  • To understand how the delay function affects the return value of the main function, it is sufficient to understand how the function returns.
Function return procedure
  • The keyword return is not an atomic operation
  • In fact, return only represents the assembly instruction return, which is about to jump to the program execution
    • For example, the statement return i
    • In fact, it is carried out in two steps
      • Store the i value in the stack as the return value
      • Then perform a jump
    • The execution time of defer is just before jump
    • Therefore, there is still a chance to manipulate the return value when defer is executed

Take a practical example:

func deferFuncReturn() (result int) {
	i := 1
	
	defer func() {
		result ++
	}()
	
	return i
}
// The return statement of this function can be split into the following two lines:
result = i
return
The main function has an anonymous return value and returns a literal value
  • A main function has an anonymous return value, which is returned using literal values
    • For example, return values such as "1", "2" and "Hello"
  • In this case, the defer statement cannot manipulate the return value
func foo() int {
    var i int
    
    defer func() {
        i ++
    }()
    
    return 1
}
// The above return statement directly writes 1 to the stack as the return value. The delay function cannot operate the return value, so it cannot affect the return value.
The main function has an anonymous return value and returns a variable
  • A main function has an anonymous return value that uses local or global variables
    • The defer statement can be referenced to the return value
    • But the return value will not be changed.
  • The following return value is not even a pointer type
func foo() int {
	var i int

	defer func() {
		i ++
		fmt.Println(i)
	}()

	return i
}
// The above function returns a local variable, and the defer function will also operate on this local variable.
// For anonymous return values
// 		You can assume that there is still a variable to store the return value
//		Assume that the return value variable is "anony"
//		The above return statement can be split into the following procedures:
annoy = i
i ++
return annoy
The main function has a named return value
  • The return value with name in the main function declaration statement will be initialized into a local variable
  • The return value can be used internally as if it were a local variable
    • If the defer statement operates on this return value
    • The returned results may be changed.
func foo() (ret int) {
	defer func() {
		ret ++
	}()

	return 0
}
// The above functions are disassembled as follows
ret = 0
ret ++
return

Implementation principle of defer

defer data structure

  • Defer must be followed by a function, so the data structure of defer is similar to that of general functions

  • There are also stack addresses, program counters, function addresses, and so on.

  • Different from function

    • It contains a pointer that can be used to point to another defer
    • Each goroutine data structure actually has a defer pointer
      • The pointer points to a single linked list of defer
      • Each time a defer is declared, the defer is inserted into the header of the single linked list
      • Each time a defer is executed, a defer is taken out from the header of the single linked list
  • A goroutine may call multiple functions in succession

  • The process of adding defer is consistent with the above process

    • Add defer when entering function
    • Remove defer when leaving function
  • Therefore, even if multiple functions are called, it can always ensure that defer is executed in FIFO mode

Creation and execution of defer

  • Source package Src / Runtime / panic Go defines two methods for creating and executing defer s
    • deferproc(): called at the place where the deferer is declared, which stores the deferer function in the linked list of goroutine
    • deferreturn(): in return instruction, it is called before ret instruction.
      • It takes the defer from the goroutine linked list and executes it
      • It can be simply understood that in the compilation stage
        • The function deferproc() is inserted at the declaration defer
        • The function deferreturn() is inserted before the function return.

summary

  • The delay function parameters defined by defer are determined when the defer statement is issued
  • defer is defined in the reverse order of actual execution
  • return is not an atomic operation
    • The execution process is: save the return value (if any) - > execute defer (if any) - > execute ret jump
  • It is a good habit to use defer to close resources immediately after applying for resources

Keywords: Go Back-end

Added by youknowho on Sat, 11 Dec 2021 11:01:54 +0200