引言
在 Go 中 defer 常用于資源的釋放赏殃,會在函數(shù)返回之前調(diào)用,經(jīng)常被用于關(guān)閉文件描述符间涵、關(guān)閉數(shù)據(jù)庫連接以及解鎖資源仁热。下面就深入 Go 語言源碼介紹 defer 關(guān)鍵字的實現(xiàn)。
目錄
- defer 關(guān)鍵字
- defer 源碼分析
- 總結(jié)
- 了解更多
1勾哩、defer 關(guān)鍵字
在 Go 中 defer 語句是一般用來做一些清理或者后置的工作抗蠢。defer 語句的執(zhí)行順序是 LIFO 規(guī)則。當(dāng) defer 與 return 一起使用時有啥需要注意的思劳,看下面的代碼迅矛,思考一下相關(guān)輸出的值。
package main
import (
"fmt"
)
func main() {
fmt.Println("anonymousVarReturn return value is", anonymousVarReturn())
fmt.Println("anonymousVarReturn2 return value is", anonymousVarReturn2(1))
fmt.Println("anonymousVarReturn3 return value is", anonymousVarReturn3(1))
fmt.Println("namedVarReturn4 return value is", namedVarReturn4())
}
func anonymousVarReturn() int {
var i = 0
defer func() {
i++
fmt.Println("anonymousVarReturn defer, i is ", i)
}()
return 0
}
func anonymousVarReturn2(i int) int {
defer func() {
i++
fmt.Println("anonymousVarReturn2 defer, i is", i)
}()
return i
}
func anonymousVarReturn3(i int) int {
defer func(i int) {
i++
fmt.Println("anonymousVarReturn3 defer, i is", i)
}(i)
return i
}
func namedVarReturn4() (i int) {
defer func(i *int) {
*i++
fmt.Println("namedVarReturn4 defer, i is", *i)
}(&i)
return 1
}
展開看下面相關(guān)值輸出潜叛,命名的返回值被 defer 修改了秽褒,是不是和你想的有點不一樣?
anonymousVarReturn defer, i is 1
anonymousVarReturn return value is 0
anonymousVarReturn2 defer, i is 2
anonymousVarReturn2 return value is 1
anonymousVarReturn3 defer, i is 2
anonymousVarReturn3 return value is 1
namedVarReturn4 defer, i is 2
namedVarReturn4 return value is 2
帶著疑惑查找了一番文檔威兜,在 go 的 Defer, Panic, and Recover 博客里發(fā)現(xiàn)了相關(guān)說明销斟。
The behavior of defer statements is straightforward and predictable. There are three simple rules:
1. A deferred function's arguments are evaluated when the defer statement is evaluated.
2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.
3. Deferred functions may read and assign to the returning function's named return values.
規(guī)則 3 就可以說明了上面代碼的返回問題了,defer 方法可能會讀取和賦值給返回的命名變量椒舵,所以 defer 方法能在 return 返回之后繼續(xù)操作同一返回變量蚂踊。
看上面的方法 anonymousVarReturn、anonymousVarReturn2笔宿、anonymousVarReturn3 和 namedVarReturn4犁钟,可以發(fā)現(xiàn)匿名的返回值是沒有被 defer 方法修改的,因為匿名的方法返回變量名是 Go 自動創(chuàng)建的泼橘,在 defer 里面就不會操作到該變量涝动。
2、defer 源碼分析
分析源碼之前需要了解 Go 的匯編相關(guān)知識侥加,要想看懂匯編內(nèi)容捧存,需要把看完下面相關(guān)鏈接的文檔,看完會對匯編有個更深的了解担败,再看下面的匯編代碼就不會那么迷糊了昔穴。
我們來簡單分析一下命名返回變量的方法的相關(guān)匯編代碼。
1 package main
2
3 import (
4 "fmt"
5 )
6
7 func main() {
8 fmt.Println("namedVarReturn4 return value is", namedVarReturn4())
9 }
10
11 func namedVarReturn4() (i int) {
12 defer func(i *int) {
13 *i++
14 fmt.Println("namedVarReturn4 defer, i is", *i)
15 }(&i)
16 return 1
17 }
本機 Go 版本是 go1.16.5 darwin/amd64提前,使用 go tool compile 工具輸出相關(guān)匯編代碼吗货,先看 help 輸出相關(guān)命令參數(shù)。
go tool compile --help
usage: compile [options] file.go...
-N disable optimizations
-S print assembly listing
-l disable inlining
…… 省略部分
運行下面命令輸出上面代碼的匯編代碼狈网。
# 輸出整個代碼的匯編指令
go tool compile -N -l -S main.go
# go build -o test main.go
# 輸出 main 方法的匯編指令
# go tool objdump -S -s main.main test
# 輸出 namedVarReturn4 方法的匯編指令
# go tool objdump -S -s main.namedVarReturn4 test
展開看下面是截取部分匯編指令宙搬,看幾行關(guān)鍵的代碼笨腥。
0x0000 00000 (main.go:11) TEXT "".namedVarReturn4(SB), ABIInternal, $96-8
0x0000 00000 (main.go:11) MOVQ (TLS), CX
0x0009 00009 (main.go:11) CMPQ SP, 16(CX)
0x000d 00013 (main.go:11) PCDATA $0, $-2
0x000d 00013 (main.go:11) JLS 129
0x000f 00015 (main.go:11) PCDATA $0, $-1
0x000f 00015 (main.go:11) SUBQ $96, SP
0x0013 00019 (main.go:11) MOVQ BP, 88(SP)
0x0018 00024 (main.go:11) LEAQ 88(SP), BP
0x001d 00029 (main.go:11) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:11) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:11) MOVQ $0, "".i+104(SP)
0x0026 00038 (main.go:12) MOVL $8, ""..autotmp_1+8(SP)
0x002e 00046 (main.go:12) LEAQ "".namedVarReturn4.func1·f(SB), AX
0x0035 00053 (main.go:12) MOVQ AX, ""..autotmp_1+32(SP)
0x003a 00058 (main.go:12) LEAQ "".i+104(SP), AX
0x003f 00063 (main.go:12) MOVQ AX, ""..autotmp_1+80(SP)
0x0044 00068 (main.go:12) LEAQ ""..autotmp_1+8(SP), AX
0x0049 00073 (main.go:12) MOVQ AX, (SP)
0x004d 00077 (main.go:12) PCDATA $1, $0
0x004d 00077 (main.go:12) CALL runtime.deferprocStack(SB) // defer 方法入棧
0x0052 00082 (main.go:15) TESTL AX, AX
0x0054 00084 (main.go:15) JNE 113
0x0056 00086 (main.go:15) JMP 88
0x0058 00088 (main.go:16) MOVQ $1, "".i+104(SP) // 這里給 i 變量賦值
0x0061 00097 (main.go:16) XCHGL AX, AX
0x0062 00098 (main.go:16) CALL runtime.deferreturn(SB) // 這里執(zhí)行 return
0x0067 00103 (main.go:16) MOVQ 88(SP), BP
0x006c 00108 (main.go:16) ADDQ $96, SP
0x0070 00112 (main.go:16) RET
0x0071 00113 (main.go:12) XCHGL AX, AX
0x0072 00114 (main.go:12) CALL runtime.deferreturn(SB)// 這里執(zhí)行 defer 方法
0x0077 00119 (main.go:15) MOVQ 88(SP), BP
0x007c 00124 (main.go:15) ADDQ $96, SP
0x0080 00128 (main.go:15) RET
0x0081 00129 (main.go:15) NOP
0x0081 00129 (main.go:11) PCDATA $1, $-1
0x0081 00129 (main.go:11) PCDATA $0, $-2
0x0081 00129 (main.go:11) CALL runtime.morestack_noctxt(SB)
0x0086 00134 (main.go:11) PCDATA $0, $-1
0x0086 00134 (main.go:11) JMP 0
…… 省略部分
"".namedVarReturn4.func1 STEXT size=298 args=0x8 locals=0x88 funcid=0x0
0x0000 00000 (main.go:12) TEXT "".namedVarReturn4.func1(SB), ABIInternal, $136-8
0x0000 00000 (main.go:12) MOVQ (TLS), CX
0x0009 00009 (main.go:12) LEAQ -8(SP), AX
0x000e 00014 (main.go:12) CMPQ AX, 16(CX)
0x0012 00018 (main.go:12) PCDATA $0, $-2
0x0012 00018 (main.go:12) JLS 288
0x0018 00024 (main.go:12) PCDATA $0, $-1
0x0018 00024 (main.go:12) SUBQ $136, SP
0x001f 00031 (main.go:12) MOVQ BP, 128(SP)
0x0027 00039 (main.go:12) LEAQ 128(SP), BP
0x002f 00047 (main.go:12) FUNCDATA $0, gclocals·2d7c1615616d4cf40d01b3385155ed6e(SB)
0x002f 00047 (main.go:12) FUNCDATA $1, gclocals·7985103f61d1dca6f16bfba926a2a610(SB)
0x002f 00047 (main.go:12) FUNCDATA $2, "".namedVarReturn4.func1.stkobj(SB)
0x002f 00047 (main.go:13) MOVQ "".i+144(SP), AX
0x0037 00055 (main.go:13) TESTB AL, (AX)
0x0039 00057 (main.go:13) MOVQ "".i+144(SP), CX
0x0041 00065 (main.go:13) TESTB AL, (CX)
0x0043 00067 (main.go:13) MOVQ (AX), AX
0x0046 00070 (main.go:13) INCQ AX
0x0049 00073 (main.go:13) MOVQ AX, (CX)
0x004c 00076 (main.go:14) XORPS X0, X0
0x004f 00079 (main.go:14) MOVUPS X0, ""..autotmp_1+96(SP)
0x0054 00084 (main.go:14) MOVUPS X0, ""..autotmp_1+112(SP)
0x0059 00089 (main.go:14) LEAQ ""..autotmp_1+96(SP), AX
0x005e 00094 (main.go:14) MOVQ AX, ""..autotmp_3+64(SP)
0x0063 00099 (main.go:14) TESTB AL, (AX)
0x0065 00101 (main.go:14) LEAQ type.string(SB), AX
0x006c 00108 (main.go:14) MOVQ AX, ""..autotmp_1+96(SP)
0x0071 00113 (main.go:14) LEAQ ""..stmp_1(SB), AX
0x0078 00120 (main.go:14) MOVQ AX, ""..autotmp_1+104(SP)
0x007d 00125 (main.go:14) MOVQ "".i+144(SP), AX
0x0085 00133 (main.go:14) TESTB AL, (AX)
0x0087 00135 (main.go:14) MOVQ (AX), AX
0x008a 00138 (main.go:14) MOVQ AX, ""..autotmp_4+48(SP)
0x008f 00143 (main.go:14) MOVQ AX, (SP)
0x0093 00147 (main.go:14) PCDATA $1, $1
0x0093 00147 (main.go:14) CALL runtime.convT64(SB)
0x0098 00152 (main.go:14) MOVQ 8(SP), AX
0x009d 00157 (main.go:14) MOVQ AX, ""..autotmp_5+56(SP)
0x00a2 00162 (main.go:14) MOVQ ""..autotmp_3+64(SP), CX
0x00a7 00167 (main.go:14) TESTB AL, (CX)
0x00a9 00169 (main.go:14) LEAQ type.int(SB), DX
0x00b0 00176 (main.go:14) MOVQ DX, 16(CX)
0x00b4 00180 (main.go:14) LEAQ 24(CX), DI
0x00b8 00184 (main.go:14) PCDATA $0, $-2
0x00b8 00184 (main.go:14) CMPL runtime.writeBarrier(SB), $0
0x00bf 00191 (main.go:14) NOP
0x00c0 00192 (main.go:14) JEQ 196
0x00c2 00194 (main.go:14) JMP 277
0x00c4 00196 (main.go:14) MOVQ AX, 24(CX)
0x00c8 00200 (main.go:14) JMP 202
0x00ca 00202 (main.go:14) PCDATA $0, $-1
0x00ca 00202 (main.go:14) MOVQ ""..autotmp_3+64(SP), AX
0x00cf 00207 (main.go:14) TESTB AL, (AX)
0x00d1 00209 (main.go:14) JMP 211
0x00d3 00211 (main.go:14) MOVQ AX, ""..autotmp_2+72(SP)
0x00d8 00216 (main.go:14) MOVQ $2, ""..autotmp_2+80(SP)
0x00e1 00225 (main.go:14) MOVQ $2, ""..autotmp_2+88(SP)
0x00ea 00234 (main.go:14) MOVQ AX, (SP)
0x00ee 00238 (main.go:14) MOVQ $2, 8(SP)
0x00f7 00247 (main.go:14) MOVQ $2, 16(SP)
0x0100 00256 (main.go:14) PCDATA $1, $2
0x0100 00256 (main.go:14) CALL fmt.Println(SB)
0x0105 00261 (main.go:15) MOVQ 128(SP), BP
0x010d 00269 (main.go:15) ADDQ $136, SP
0x0114 00276 (main.go:15) RET
0x0115 00277 (main.go:14) PCDATA $0, $-2
0x0115 00277 (main.go:14) CALL runtime.gcWriteBarrier(SB)
0x011a 00282 (main.go:14) JMP 202
0x011c 00284 (main.go:14) NOP
0x011c 00284 (main.go:12) PCDATA $1, $-1
0x011c 00284 (main.go:12) PCDATA $0, $-2
0x011c 00284 (main.go:12) NOP
0x0120 00288 (main.go:12) CALL runtime.morestack_noctxt(SB)
0x0125 00293 (main.go:12) PCDATA $0, $-1
0x0125 00293 (main.go:12) JMP 0
從上面的匯編指令里可以看出執(zhí)行流程首先會調(diào)用 deferprocStack 來創(chuàng)建 defer,然后在函數(shù)返回時 (指令 JMP 0)
插入了指令 CALL runtime.deferreturn(SB)
勇垛。
知道了 defer 在流程中是通過這兩個方法是調(diào)用的脖母,知道了調(diào)用的名字,可以直接復(fù)制在 Go 源碼里全局搜索接一下 deferprocStack闲孤。
下面是 deferprocStack 的源碼
// src/runtime/panic.go
// deferprocStack queues a new deferred function with a defer record on the stack.
// The defer record must have its siz and fn fields initialized.
// All other fields can contain junk.
// The defer record must be immediately followed in memory by
// the arguments of the defer.
// Nosplit because the arguments on the stack won't be scanned
// until the defer record is spliced into the gp._defer list.
//go:nosplit
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
下面是 deferprocStack 方法傳入的 _defer 的源碼
// src/runtime/runtime2.go
// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer and deferProcStack
// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
// and cmd/compile/internal/gc/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer
// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}
下面是 deferreturn 的源碼
// src/runtime/runtime2.go
// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
//
// Declared as nosplit, because the function should not be preempted once we start
// modifying the caller's frame in order to reuse the frame to call the deferred
// function.
//
// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
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)
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
上面 deferreturn 方法里最后調(diào)用里 jmpdefer 方法谆级,因為本機是64位的,所以對應(yīng) asm_amd64.s 匯編指令讼积, 下面是 jmpdefer 的實現(xiàn)
//src/runtime/asm_amd64.s
// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
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
配合源碼注釋以及下面相關(guān)鏈接的文檔可以理解出大概意思肥照。有興趣的可以去查閱一下相關(guān)知識再來深入了解。
3勤众、總結(jié)
最后在總結(jié)一下 defer 的流程舆绎。
- 編譯器會把 defer 語句翻譯成對 deferprocStack 函數(shù)的調(diào)用。
- deferprocStack 函數(shù)會把 _defer 結(jié)構(gòu)體對象并放入當(dāng)前的 goroutine 的 _defer 鏈表们颜。
- 編譯器會在 defer 所在函數(shù)的結(jié)尾處插入對 deferreturn 的調(diào)用吕朵,deferreturn 遞歸調(diào)用 defer 語句所在函數(shù)。
相關(guān)鏈接
- Defer, Panic, and Recover
- plan9-assembly-完全解析
- A Quick Guide to Go's Assembler
- Chapter I: Go Assembly
- golang 匯編
- Go 語言
- A Manual for the Plan 9 assembler
-
中文版-Plan9匯編器手冊
4窥突、了解更多
原文鏈接:Golang 中 defer 機制分析