前言
!! 嗨,大家好,我是asong,鴿了好久,其實元旦就想寫一下這篇文章,但是因為喝酒喝斷片了,養了三天才緩過來,就推遲到這個周末了,不過多追溯了,有點丢人。今天與大家來聊一聊中的關鍵字
go
,目前很多程式設計語言中都有
defer
關鍵字,而
defer
語言的
go
用于資源的釋放,會在函數傳回之前進行調用,它會經常被用于關閉檔案描述符、關閉資料庫連接配接以及解鎖資源。下面我們就深入
defer
語言源碼介紹
Go
關鍵字的實作原理。文末尾給你們留了三道題,檢測一下學習成果吧~
defer
基本使用
我們首先來看一看
defer
關鍵字是怎麼使用的,一個經典的場景就是我們在使用事務時,發生錯誤需要復原,這時我們就可以用使用defer來保證程式退出時保證事務復原,示例代碼如下:
// 代碼摘自之前寫的 Leaf-segment資料庫擷取ID方案:https://github.com/asong2020/go-algorithm/blob/master/leaf/dao/leaf_dao.go
func (l *LeafDao) NextSegment(ctx context.Context, bizTag string) (*model.Leaf, error) {
// 開啟事務
tx, err := l.sql.Begin()
defer func() {
if err != nil {
l.rollback(tx)
}
}()
if err = l.checkError(err); err != nil {
return nil, err
}
err = l.db.UpdateMaxID(ctx, bizTag, tx)
if err = l.checkError(err); err != nil {
return nil, err
}
leaf, err := l.db.Get(ctx, bizTag, tx)
if err = l.checkError(err); err != nil {
return nil, err
}
// 送出事務
err = tx.Commit()
if err = l.checkError(err); err != nil {
return nil, err
}
return leaf, nil
}
上面隻是一個簡單的應用,
defer
還有一些特性,如果你不知道,使用起來可能會踩到一些坑,尤其是跟帶命名的傳回參數一起使用時。下面我們我先來帶大家踩踩坑。
defer
的注意事項和細節
defer
defer
調用順序
defer
我們先來看一道題,你能說他的答案嗎?
func main() {
fmt.Println("reciprocal")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
}
答案:
reciprocal
9
8
7
6
5
4
3
2
1
0
看到答案,你是不是産生了疑問?這就對了,我最開始學
golang
時也有這個疑問,這個跟棧一樣,即"先進後出"特性,越後面的defer表達式越先被調用。是以這裡大家關閉依賴資源時一定要注意
defer
調用順序。
defer
拷貝
defer
我們先來看這樣一段代碼,你能說出
defer
中
num1
和
num2
的值是多少嗎?
func main() {
fmt.Println(Sum(1, 2))
}
func Sum(num1, num2 int) int {
defer fmt.Println("num1:", num1)
defer fmt.Println("num2:", num2)
num1++
num2++
return num1 + num2
}
聰明的你一定會說:"這也太簡單了,答案就是num1等于2,num2等于3"。很遺憾的告訴你,錯了,正确的答案是
num1
為
1
,
num2
為2,這兩個變量并不受
num1++、num2++
的影響,因為
defer
将語句放入到棧中時,也會将相關的值拷貝同時入棧。
defer
與 return
的傳回時機
defer
return
這裡我先說結論,總結一下就是,函數的整個傳回過程應該是:
-
對傳回變量指派,如果是匿名傳回值就先聲明再指派;return
- 執行
函數;defer
-
攜帶傳回值傳回。return
下面我們來看兩道題,你知道他們的傳回值是多少嗎?
- 匿名傳回值函數
// 匿名函數
func Anonymous() int {
var i int
defer func() {
i++
fmt.Println("defer2 value is ", i)
}()
defer func() {
i++
fmt.Println("defer1 in value is ", i)
}()
return i
}
- 命名傳回值的函數
func HasName() (j int) {
defer func() {
j++
fmt.Println("defer2 in value", j)
}()
defer func() {
j++
fmt.Println("defer1 in value", j)
}()
return j
}
先來公布一下答案吧:
1. Anonymous()的傳回值為0
2. HasName()的傳回值為2
從這我們可以看出命名傳回值的函數的傳回值被
defer
修改了。這裡想必大家跟我一樣,都很疑惑,帶着疑惑我查閱了一下
go
官方文檔,文檔指出,
defer
的執行順序有以下三個規則:
- A deferred function’s arguments are evaluated when the defer statement is evaluated.
- Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- Deferred functions may read and assign to the returning function’s named return values.
規則3就可以印證為什麼命名傳回值的函數的傳回值被更改了,其實在函數最終傳回前,
defer
函數就已經執行了,在命名傳回值的函數 中,由于傳回值已經被提前聲明,是以
defer
函數能夠在
return
語句對傳回值指派之後,繼續對傳回值進行操作,操作的是同一個變量,而匿名傳回值函數中return先傳回,已經進行了一次值拷貝r=i,
defer
函數中再次對變量
i
的操作并不會影響傳回值。
這裡可能有些小夥伴還不是很懂,我在講一下
return
傳回步驟,相信你們會豁然開朗。
- 函數在傳回時,首先函數傳回時會自動建立一個傳回變量假設為ret(如果是命名傳回值的函數則不會建立),函數傳回時要将變量
指派給ret,即有ret = i。i
- 然後檢查函數中是否有defer存在,若有則執行defer中部分。
- 最後傳回ret
現在你們應該知道上面是什麼原因了吧~。
解密 defer
源碼
defer
寫在開頭:go版本1.15.3
我們先來寫一段代碼,檢視一下彙編代碼:
func main() {
defer func() {
fmt.Println("asong 真帥")
}()
}
執行如下指令:
go tool compile -N -l -S main.go
,截取部分彙編指令如下:
我們可以看出來,從執行流程來看首先會調用
deferproc
來建立
defer
,然後在函數傳回時插入了指令
CALL runtime.deferreturn
。知道了
defer
在流程中是通過這兩個方法是調用的,接下來我們來看一看
defer
的結構:
// go/src/runtime/runtime2.go
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
}
這裡簡單介紹一下
runtime._defer
結構體中的幾個字段:
-
代表的是參數和結果的記憶體大小siz
-
和sp
分别代表棧指針和調用方的程式計數器pc
-
代表的是fn
關鍵字中傳入的函數defer
-
是觸發延遲調用的結構體,可能為空_panic
-
表示的是目前openDefer
是否已經開放編碼優化(1.14版本新增)defer
-
所有link
結構體都通過該字段串聯成連結清單runtime._defer
先來我們也知道了
defer
關鍵字的資料結構了,下面我們就來重點分析一下
deferproc
和
deferreturn
函數是如何調用。
deferproc
函數
deferproc
deferproc
函數也不長,我先貼出來代碼;
// proc/panic.go
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// the arguments of fn are in a perilous state. The stack map
// for deferproc does not describe them. So we can't let garbage
// collection or stack copying trigger until we've copied them out
// to somewhere safe. The memmove below does that.
// Until the copy completes, we can only call nosplit routines.
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
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))
}
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
上面介紹了
rumtiem._defer
結構想必這裡的入參是什麼意思就不用我介紹了吧。
deferproc
的函數流程很清晰,首先他會通過
newdefer
函數配置設定一個
_defer
結構對象,然後把需要延遲執行的函數以及該函數需要用到的參數、調用
deferproc
函數時的
rps
寄存器的值以及
deferproc
函數的傳回位址儲存在
_defer
結構體對象中,最後通過
return0()
設定
rax
寄存器的值為0隐性的給調用者傳回一個0值。
deferproc
主要是靠
newdefer
來配置設定
_defer
結構體對象的,下面我們一起來看看
newdefer
實作,代碼有點長:
// proc/panic.go
// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer. The defer is not
// added to any defer chain yet.
//
// This must not grow the stack because there may be a frame without
// stack map information when this is called.
//
//go:nosplit
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()//擷取目前goroutine的g結構體對象
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr() //與目前工作線程綁定的p
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// Take the slow path on the system stack so
// we don't grow newdefer's stack.
systemstack(func() {
lock(&sched.deferlock)
//把新配置設定出來的d放入目前goroutine的_defer連結清單頭
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil {
//如果p的緩存中沒有可用的_defer結構體對象則從堆上配置設定
// Allocate new defer+args.
//因為roundupsize以及mallocgc函數都不會處理擴棧,是以需要切換到系統棧執行
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
if debugCachedWork {
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
//把新配置設定出來的d放入目前goroutine的_defer連結清單頭
d.link = gp._defer
gp._defer = d
return d
}
}
d.siz = siz
d.heap = true
return d
}
newdefer
函數首先會嘗試從目前工作線程綁定的
p
的
_defer
對象池和全局對象池中擷取一個滿足大小要求
(sizeof(_defer) + siz向上取整至16的倍數)
的
_defer
結構體對象,如果沒有能夠滿足要求的空閑
_defer
對象則從堆上分一個,最後把配置設定到的對象鍊入目前
goroutine
的
_defer
連結清單的表頭。
到此
deferproc
函數就分析完了,你們懂了嗎? 沒懂不要緊,我們再來總結一下這個過程:
- 首先編譯器把
語句翻譯成對應的defer
函數的調用deferproc
- 然後
函數通過deferproc
函數配置設定一個newdefer
結構體對象并放入目前的_defer
的goroutine
連結清單的表頭;_defer
- 在 _defer 結構體對象中儲存被延遲執行的函數 fn 的位址以及 fn 所需的參數
- 傳回到調用 deferproc 的函數繼續執行後面的代碼。
deferreturn
函數
deferreturn
// 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() //擷取目前goroutine對應的g結構體對象
d := gp._defer //擷取目前goroutine對應的g結構體對象
if d == nil {
//沒有需要執行的函數直接傳回,deferreturn和deferproc是配對使用的
//為什麼這裡d可能為nil?因為deferreturn其實是一個遞歸調用,這個是遞歸結束條件之一
return
}
sp := getcallersp() //擷取調用deferreturn時的棧頂位置
if d.sp != sp { // 遞歸結束條件
//如果儲存在_defer對象中的sp值與調用deferretuen時的棧頂位置不一樣,直接傳回
//因為sp不一樣表示d代表的是在其他函數中通過defer注冊的延遲調用函數,比如:
//a()->b()->c()它們都通過defer注冊了延遲函數,那麼當c()執行完時隻能執行在c中注冊的函數
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.
//把儲存在_defer對象中的fn函數需要用到的參數拷貝到棧上,準備調用fn
//注意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 // 使gp._defer指向下一個_defer結構體對象
//因為需要調用的函數d.fn已經儲存在了fn變量中,它的參數也已經拷貝到了棧上,是以釋放_defer結構體對象
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
函數主要流程還是簡單一些的,我們來分析一下:
- 首先我們通過目前
對應的goroutine
結構體對象的g
連結清單判斷是否有需要執行的_defer
函數,如果沒有則傳回;這裡的沒有是指g._defer== nil 或者defered
函數不是在defered
的deferteturn
函數中注冊的函數。caller
- 然後我們在從
對象中把_defer
函數需要的參數拷貝到棧上,并釋放defered
的結構體對象。_defer
- 最紅調用
函數調用jmpderfer
函數,也就是defered
關鍵字中傳入的函數.defer
jmpdefer
函數實作挺優雅的,我們一起來看看他是如何實作的:
// runtime/asm_amd64.s : 581
// 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
這裡都是彙編,大家可能看不懂,沒關系,我隻是簡單介紹一下這裡,有興趣的同學可以去查閱一下相關知識再來深入了解。
-
這條指令會把MOVQ fv+0(FP), DX
的第一個參數也就是結構體對象jmpdefer
的位址存放入fn
寄存器,之後的代碼就可以通過寄存器通路到DX
,fn
就可以拿到fn
關鍵字中傳入的函數,對應上面的例子就是匿名函數defer
.func(){}()
-
這條指令就是把MOVQ argp+8(FP), BX
的第二個參數放入jmpdefer
寄存器,該參數是一個指針,他指向BX
關鍵字中傳入的函數的第一個參數.defer
-
這條指令的作用是讓 LEAQ -8(BX), SP
寄存器指向 SP
函數的傳回位址所在的棧記憶體單元.deferreturn
-
這條指令的作用是調整 BP 寄存器的值,此時MOVQ -8(SP), BP
的位置存放的是SP_8
關鍵字目前所在的函數的defer
寄存器的值,是以這條指令在調整rbp
寄存器的值使其指向目前所在函數的棧幀的适當位置.rbp
-
這裡的作用是完成SUBQ $5, (SP)
函數的參數以及執行完函數後傳回位址在棧上的構造.因為在執行這條指令時,rsp寄存器指向的是defer
函數的傳回位址.deferreturn
-
和MOVQ 0(DX), BX
放到一起說吧,目的是跳轉到對應JMP BX
函數去執行,完成defer
函數的調用.defer
總結
大概分析了一下
defer
的實作機制,但還是有點蒙圈,最後在總結一下這裡:
- 首先編譯器會把
語句翻譯成對defer
函數的調用。deferproc
- 然後
函數會負責調用deferproc
函數配置設定一個newdefer
結構體對象并放入目前的_defer
的goroutine
連結清單的表頭;_defer
- 然後編譯起會在
所在函數的結尾處插入對defer
的調用,deferreturn
負責遞歸的調用某函數(defer語句所在函數)通過deferreturn
語句注冊的函數。defer
總體來說就是這三個步驟,go語言對
defer
的實作機制就是這樣啦,你明白了嗎?
小試牛刀
上面我們也細緻學習了一下
defer
,下面出幾道題吧,看看你們真的學會了嗎?
問題1
// 測試1
func Test1() (r int) {
i := 1
defer func() {
i = i + 1
}()
return i
}
傳回結果是什麼?
問題2
func Test2() (r int) {
defer func(r int) {
r = r + 2
}(r)
return 2
}
傳回結果是什麼?
如果改成這樣呢?
func Test3() (r int) {
defer func(r *int) {
*r = *r + 2
}(&r)
return 2
}
問題3
func main(){
e1()
e2()
e3()
}
func e1() {
var err error
defer fmt.Println(err)
err = errors.New("e1 defer err")
}
func e2() {
var err error
defer func() {
fmt.Println(err)
}()
err = errors.New("e2 defer err")
}
func e3() {
var err error
defer func(err error) {
fmt.Println(err)
}(err)
err = errors.New("e3 defer err")
}
這個傳回結果又是什麼呢?
總結
最後這三道題這裡就當作思考題吧,自己運作一下,看看你們想的和運作結果是否一緻呢?