In the previous chapterIn-depth understanding of Go panic and recoverWe found outdefer
With its great relevance, I still think it is very necessary to go deeper. I hope that through this chapter you can be rightdefer
Keyword has a profound understanding, so let’s begin. You wait first, please line up. We use LIFO here …
Original address:In-depth understanding of Go defer
Characteristics
Let’s have a quick lookdefer
The basic use of keywords allows everyone to have a basic understanding first.
I. Delayed Call
func main() {
defer log.Println("EDDYCJY.")
log.Println("end.")
}
Output results:
$ go run main.go
2019/05/19 21:15:02 end.
2019/05/19 21:15:02 EDDYCJY.
Second, LIFO
func main() {
for i := 0; i < 6; i++ {
defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".")
}
log.Println("end.")
}
Output results:
$ go run main.go
2019/05/19 21:19:17 end.
2019/05/19 21:19:17 EDDYCJY5.
2019/05/19 21:19:17 EDDYCJY4.
2019/05/19 21:19:17 EDDYCJY3.
2019/05/19 21:19:17 EDDYCJY2.
2019/05/19 21:19:17 EDDYCJY1.
2019/05/19 21:19:17 EDDYCJY0.
III. Operating Time Points
func main() {
func() {
defer log.Println("defer.EDDYCJY.")
}()
log.Println("main.EDDYCJY.")
}
Output results:
$ go run main.go
2019/05/22 23:30:27 defer.EDDYCJY.
2019/05/22 23:30:27 main.EDDYCJY.
IV. Exception Handling
func main() {
defer func() {
if e := recover(); e != nil {
log.Println("EDDYCJY.")
}
}()
panic("end.")
}
Output results:
$ go run main.go
2019/05/20 22:22:57 EDDYCJY.
Source code analysis
$ go tool compile -S main.go
"".main STEXT size=163 args=0x0 locals=0x40
...
0x0059 00089 (main.go:6) MOVQ AX, 16(SP)
0x005e 00094 (main.go:6) MOVQ $1, 24(SP)
0x0067 00103 (main.go:6) MOVQ $1, 32(SP)
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB)
0x0075 00117 (main.go:6) TESTL AX, AX
0x0077 00119 (main.go:6) JNE 137
0x0079 00121 (main.go:7) XCHGL AX, AX
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
0x0084 00132 (main.go:7) ADDQ $64, SP
0x0088 00136 (main.go:7) RET
0x0089 00137 (main.go:6) XCHGL AX, AX
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB)
0x008f 00143 (main.go:6) MOVQ 56(SP), BP
0x0094 00148 (main.go:6) ADDQ $64, SP
0x0098 00152 (main.go:6) RET
...
First we need to find it and find out what execution code it actually corresponds to. By compiling the code, we can know that the following methods are involved:
- runtime.deferproc
- runtime.deferreturn
Obviously, it is the runtime method and the right person. Let’s continue to go down and see what actions have been taken respectively.
Data structure
Before we begin, we need to introducedefer
The basic unit of_defer
Structure, as follows:
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
...
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
- Siz: Total Size of All Incoming Parameters
- Started: It’s time
defer
Has it been implemented - Sp: Function Stack Pointer Register, which generally points to the top of the current function stack
- Pc: Program counter, sometimes called instruction pointer (IP), which threads use to track the next instruction to be executed. In most processors, the PC points to the next instruction, not the current instruction
- Fn: Point to the function address and parameters passed in
- _panic: pointing to
_panic
Linked list - Link: point to
_defer
Linked list
deferproc
func deferproc(siz int32, fn *funcval) {
...
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
...
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
- Get call
defer
The function stack pointer of the function, the specific address of the parameter of the incoming function, and PC (program counter), which is the next instruction to be executed. These are equivalent to preparatory parameters for subsequent flow control. - Create a new
defer
Minimum cell_defer
To fill in the previously prepared parameters - call
memmove
Stores the passed-in parameters to the new_defer
(currently in use) to facilitate subsequent use - Last call
return0
To return, this function is very important. Can be avoided indeferproc
Because of the returnreturn
, and induceddeferreturn
The call to the method. The root cause is a haltpanic
The delay method of enables thedeferproc
Returns 1, but in the mechanism ifdeferproc
If the return value is not equal to 0, it will always check the return value and jump to the end of the function. Andreturn0
The return is 0, so repeated calls can be prevented.
Summary
InThis function will be new_defer
Set some basic properties and pass in the parameter set of the calling function. Finally, the function call is ended by a special return method. In addition, this one is similar to the previous one.In-depth understanding of Go panic and recoverThe processing logic of has certain relevance, in fact isgp.sched.ret
Returns 0 or 1 will be diverted to different processing methods
newdefer
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
...
lock(&sched.deferlock)
d := sched.deferpool[sc]
unlock(&sched.deferlock)
}
...
}
if d == nil {
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
...
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
- Get available from the pool
_defer
, reuse as a new base unit - If there is no available in the pool, call
mallocgc
Re-apply for a new one - Set up
defer
Finally, modify the currentGoroutine
The_defer
point to
Through this method, we can notice two points, as follows:
-
defer
AndGoroutine(g)
There is a direct relationship, so the discussiondefer
When basic cannot leaveg
The association of - New
defer
It will always be at the front of the existing linked list, that isdefer
Last in, first out
Summary
This function is mainly responsible for obtaining the new_defer
The role of it may be fromdeferpool
It is also possible to apply again
deferreturn
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
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
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
If called in a methoddefer
Keyword, the compiler will insert at the enddeferreturn
The call to the method. This method mainly includes the following steps:
- Clear current node
_defer
Called function call information - The that releases the current node
_defer
, and put it back into the pool (easy to reuse) - Jump to call
defer
At the calling function of the keyword
In this code, the jump methodjmpdefer
Especially important. Because it explicitly controls the flow, the code is as follows:
// asm_amd64.s
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn
MOVQ argp+8(FP), BX // caller sp
LEAQ -8(BX), SP // caller sp after CALL
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
SUBQ $5, (SP) // return to CALL again
MOVQ 0(DX), BX
JMP BX // but first run the deferred function
Through the analysis of the source code, we found that it did two very “strange” and important things, as follows:
- MOVQ -8(SP), BP:
-8(BX)
This location holdsdeferreturn
Address after execution - SUBQ $5, (SP):
SP
If you subtract 5 from your address, the length you subtract is exactlyruntime.deferreturn
The length of
You may ask, why is it 5? All right. After half a day of searching, I finally looked at the assembly code … well, subtraction is definitely 5. there is nothing wrong with it, as follows:
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
Let’s sort out our thoughts, according to the above logic, thendeferreturn
Is a “recursive” oh. Every time I go backdeferreturn
Function, then when will it end, as follows:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
...
}
That is, will continue to enterdeferreturn
Function to determine whether there is still a list_defer
. If it no longer exists, return and end it. To put it simply, it is to deal with the whole department.defer
To allow you to really leave it. Is this really the case? Let’s look at the assembly code above, as follows:
。..
0x0070 00112 (main.go:6) CALL runtime.deferproc(SB)
0x0075 00117 (main.go:6) TESTL AX, AX
0x0077 00119 (main.go:6) JNE 137
0x0079 00121 (main.go:7) XCHGL AX, AX
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB)
0x007f 00127 (main.go:7) MOVQ 56(SP), BP
0x0084 00132 (main.go:7) ADDQ $64, SP
0x0088 00136 (main.go:7) RET
0x0089 00137 (main.go:6) XCHGL AX, AX
0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB)
...
It is true that the analysis is consistent with the above process, and the verification is completed.
Summary
This function is mainly responsible for emptying the useddefer
And jump to calldefer
Keyword function, is very important
Summary
We mentioned thatdefer
Keyword involves two core functions, namelydeferproc
Anddeferreturn
Function. Anddeferreturn
Function is special, when application function callsdefer
Keyword, the compiler inserts at the end of itdeferreturn
The call of the, they are usually in pairs
But when oneGoroutine
There are many times in the worlddefer
Behavior (i.e. multiple_defer
), the compiler will make use of some small tricks to go back todeferreturn
Function to consume_defer
The linked list is not allowed to end until there is none left.
The new basic unit_defer
, may be reused, may also be a new application. It will eventually be appended to the_defer
The header of the linked list, thus setting the last-in-first-out calling feature.