Golang source code reading notes - Defer

When it comes to defer, you first need to understand its usage rules. The defer statement meets three principles:

defer rule

1. The parameters of the delay function have been determined when the defer function appears

func test1() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

=> The print result is 0

In the above experimental function 1, when defer appears, the value of the parameter i of the delay function fmt.Println has been determined, which is 0 at this time; Therefore, even if the value of i is updated after defer, the final result printed by defer is still 0

func test2(){
	i := map[string]int64{
		"1": 1,
		"2": 2,
	}
	defer fmt.Println(i)
	i["3"] = 3
	return
}

=> The print result is map[1:1, 2:2, 3:3]

In the above experimental function 2, because i is the variable map of reference type at this time, the reference of map is determined when the defer statement appears. After the map is modified, the defer will still perceive the change during execution, that is:

For pointer type parameters, the rule still applies, except that the parameter of the delay function is an address value. In this case, the modification of variables in the statement after defer may affect the delay function.

2. The delay function is executed in the order of last in first out, that is, the defer that appears first is executed last

The definition of the defer function is similar to the operation of entering the stack. It performs the operation similar to the operation of exiting the stack, last in first out;

Each goroutine maintains a defer linked list for executing defer functions

3. The delay function may operate on the named return value of the main function

This principle involves the process of function return. First of all, it should be clear that the return function is not an atomic operation

func test3() (i int64) {
	i = 0
	defer func() { i++ }()
	return i
}

fmt.Println(test3()) => 1

The function return is divided into two steps. Take return i in test3 function as an example:

  • The first step is to store the value of i in the stack as the return value
  • The second part is to execute the jump program execution instruction ret

The execution of the defer function is just after the first step and before the second step, so defer can modify the return value of the named function

Implementation principle of defer

Underlying data structure

type _defer struct {
	siz     int32     // defer function parameter size
	started bool 
	heap    bool
	openDefer bool
	sp        uintptr  // sp calling the function
	pc        uintptr // pc calling function
	fn        *funcval  // defer delay function pointer
	_panic    *_panic // Triggered panic information
	link      *_defer // defer function linked list
}

Def creation

The creation process of defer is defined in the deferproc() function in runtime/panic.go

// @titile: deferproc
// @description: create a defer function
// @param:
// 		siz: int32. Parameter size, bytes
// 		fn: *funcval. Function pointer
func deferproc(siz int32, fn *funcval) {
	// Get current goroutine
	gp := getg()
	...
	// getcallersp returns the stack pointer (SP) of its caller's caller
	// Gets the stack pointer of the calling function (the function calling defer)
	sp := getcallersp()
	
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	
	// getcallerpc returns the program counter (PC) of its caller's caller
	// Gets the program counter of the calling function
	callerpc := getcallerpc()
	
	// Create a new_ defer object
	d := newdefer(siz)
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
	
	// This two-step linked list operation will create a new_ Put the defer object into the chain header of goroutine
	d.link = gp._defer
	gp._defer = d
	
	d.fn = fn
	d.pc = callerpc
	d.sp = sp
	
	// Handle the parameters and copy the parameters of the specified size to_ Parameter area corresponding to defer object
	switch siz {
	case 0:
	case sys.PtrSize:
		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
	default:
		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
	}
	return0()
}

Implementation of defer

The execution process of defer is defined in the deferreturn() function in runtime/panic.go

// @title: deferreturn
// @description: defer execution function (some codes are the same as deferproc, so there is no comment)
// @param:
// 		arg0: uintptr. Address to store delay function parameters
func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}
	// This parameter is not familiar. Skip it temporarily
	if d.openDefer {
		...
	}
	
	// Copy the parameters of the delay function to args0
	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
	default:
		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
	}
	fn := d.fn
	d.fn = nil
	
	// Take the current defer object out of the linked list, and goroutine directly points to the next object that the current defer points to_ Defer object
	// Create_ Put the defer into the chain header, take it out from the header, and realize the last in first out stack model
	gp._defer = d.link
	
	// The Freeder function mainly clears the current_ Parameter of the defer object to ensure that it can no longer be executed
	freedefer(d)
	// Execution delay function
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

From the creation and execution process of defer, we can summarize the following points:

  1. The defer function is registered in the current goroutine_ The last in first out stack model is implemented through a linked list
    • It also shows that the scope of the defer function is in the current goroutine and cannot be executed across goroutines

Tags: Go

Posted on Fri, 01 Oct 2021 14:51:41 -0400 by jaykappy