舉個例子,如下
創(chuàng)新互聯(lián)建站專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都做網(wǎng)站、網(wǎng)站建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)、桐鄉(xiāng)網(wǎng)絡(luò)推廣、微信小程序定制開發(fā)、桐鄉(xiāng)網(wǎng)絡(luò)營銷、桐鄉(xiāng)企業(yè)策劃、桐鄉(xiāng)品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎;創(chuàng)新互聯(lián)建站為所有大學(xué)生創(chuàng)業(yè)者提供桐鄉(xiāng)建站搭建服務(wù),24小時服務(wù)熱線:13518219792,官方網(wǎng)址:www.cdcxhl.com
答案
解析:
defer函數(shù)在處理Panic() 和Recover()時的應(yīng)用
panic 函數(shù)是內(nèi)置的go函數(shù),它 終止 go程序的當(dāng)前流程并開始 panicking , recover 函數(shù)也是內(nèi)置的一個go函數(shù),允許你收回處理那些使用了 panic 函數(shù)的 goroutine 的控制權(quán)
來個案例
GO中的defer會在當(dāng)前函數(shù)返回前執(zhí)行傳入的函數(shù),常用于關(guān)閉文件描述符,關(guān)閉鏈接及解鎖等操作。
Go語言中使用defer時會遇到兩個常見問題:
接下來我們來詳細(xì)處理這兩個問題。
官方有段對defer的解釋:
這里我們先來一道經(jīng)典的面試題
你覺得這個會打印什么?
輸出結(jié)果:
這里是遵循先入后出的原則,同時保留當(dāng)前變量的值。
把這道題簡化一下:
輸出結(jié)果
上述代碼輸出似乎不符合預(yù)期,這個現(xiàn)象出現(xiàn)的原因是什么呢?經(jīng)過分析,我們發(fā)現(xiàn)調(diào)用defer關(guān)鍵字會立即拷貝函數(shù)中引用的外部參數(shù),所以fmt.Println(i)的這個i是在調(diào)用defer的時候就已經(jīng)賦值了,所以會直接打印1。
想要解決這個問題也很簡單,只需要向defer關(guān)鍵字傳入匿名函數(shù)
這里把一些垃圾回收使用的字段忽略了。
中間代碼生成階段cmd/compile/internal/gc/ssa.go會處理程序中的defer,該函數(shù)會根據(jù)條件不同,使用三種機(jī)制來處理該關(guān)鍵字
開放編碼、堆分配和棧分配是defer關(guān)鍵字的三種方法,而Go1.14加入的開放編碼,使得關(guān)鍵字開銷可以忽略不計。
call方法會為所有函數(shù)和方法調(diào)用生成中間代碼,工作內(nèi)容:
defer關(guān)鍵字在運(yùn)行時會調(diào)用deferproc,這個函數(shù)實現(xiàn)在src/runtime/panic.go里,接受兩個參數(shù):參數(shù)的大小和閉包所在的地址。
編譯器不僅將defer關(guān)鍵字轉(zhuǎn)成deferproc函數(shù),還會通過以下三種方式為所有調(diào)用defer的函數(shù)末尾插入deferreturn的函數(shù)調(diào)用
1、在cmd/compile/internal/gc/walk.go的walkstmt函數(shù)中,在遇到ODEFFER節(jié)點時會執(zhí)行Curfn.Func.SetHasDefer(true),設(shè)置當(dāng)前函數(shù)的hasdefer屬性
2、在ssa.go的buildssa會執(zhí)行s.hasdefer = fn.Func.HasDefer()更新hasdefer
3、在exit中會根據(jù)hasdefer在函數(shù)返回前插入deferreturn的函數(shù)調(diào)用
runtime.deferproc為defer創(chuàng)建了一個runtime._defer結(jié)構(gòu)體、設(shè)置它的函數(shù)指針fn、程序計數(shù)器pc和棧指針sp并將相關(guān)參數(shù)拷貝到相鄰的內(nèi)存空間中
最后調(diào)用的return0是唯一一個不會觸發(fā)延遲調(diào)用的函數(shù),可以避免deferreturn的遞歸調(diào)用。
newdefer的分配方式是從pool緩存池中獲?。?/p>
這三種方式取到的結(jié)構(gòu)體_defer,都會被添加到鏈表的隊頭,這也是為什么defer按照后進(jìn)先出的順序執(zhí)行。
deferreturn就是從鏈表的隊頭取出并調(diào)用jmpdefer傳入需要執(zhí)行的函數(shù)和參數(shù)。
該函數(shù)只有在所有延遲函數(shù)都執(zhí)行后才會返回。
如果我們能夠?qū)⒉糠纸Y(jié)構(gòu)體分配到棧上就可以節(jié)約內(nèi)存分配帶來的額外開銷。
在call函數(shù)中有在棧上分配
在運(yùn)行期間deferprocStack只需要設(shè)置一些未在編譯期間初始化的字段,就可以將棧上的_defer追加到函數(shù)的鏈表上。
除了分配的位置和堆的不同,其他的大致相同。
Go語言在1.14中通過開放編碼實現(xiàn)defer關(guān)鍵字,使用代碼內(nèi)聯(lián)優(yōu)化defer關(guān)鍵的額外開銷并引入函數(shù)數(shù)據(jù)funcdata管理panic的調(diào)用,該優(yōu)化可以將 defer 的調(diào)用開銷從 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而開放編碼作為一種優(yōu)化 defer 關(guān)鍵字的方法,它不是在所有的場景下都會開啟的,開放編碼只會在滿足以下的條件時啟用:
如果函數(shù)中defer關(guān)鍵字的數(shù)量多于8個或者defer處于循環(huán)中,那么就會禁用開放編碼優(yōu)化。
可以看到這里,判斷編譯參數(shù)不用-N,返回語句的數(shù)量和defer數(shù)量的乘積小于15,會啟用開放編碼優(yōu)化。
延遲比特deferBitsTemp和延遲記錄是使用開放編碼實現(xiàn)defer的兩個最重要的結(jié)構(gòu),一旦使用開放編碼,buildssa會在棧上初始化大小為8個比特的deferBits
延遲比特中的每一個比特位都表示該位對應(yīng)的defer關(guān)鍵字是否需要被執(zhí)行。延遲比特的作用就是標(biāo)記哪些defer關(guān)鍵字在函數(shù)中被執(zhí)行,這樣就能在函數(shù)返回時根據(jù)對應(yīng)的deferBits確定要執(zhí)行的函數(shù)。
而deferBits的大小為8比特,所以該優(yōu)化的條件就是defer的數(shù)量小于8.
而執(zhí)行延遲調(diào)用的時候仍在deferreturn
這里做了特殊的優(yōu)化,在runOpenDeferFrame執(zhí)行開放編碼延遲函數(shù)
1、從結(jié)構(gòu)體_defer讀取deferBits,執(zhí)行函數(shù)等信息
2、在循環(huán)中依次讀取執(zhí)行函數(shù)的地址和參數(shù)信息,并通過deferBits判斷是否要執(zhí)行
3、調(diào)用reflectcallSave執(zhí)行函數(shù)
1、新加入的defer放入隊頭,執(zhí)行defer時是從隊頭取函數(shù)調(diào)用,所以是后進(jìn)先出
2、通過判斷defer關(guān)鍵字、return數(shù)量來判斷是否開啟開放編碼優(yōu)化
3、調(diào)用deferproc函數(shù)創(chuàng)建新的延遲調(diào)用函數(shù)時,會立即拷貝函數(shù)的參數(shù),函數(shù)的參數(shù)不會等到真正執(zhí)行時計算
什么是defer
defer 可以保證方法可以在外圍函數(shù)返回之前調(diào)用。有點像其他言的 try finally
Go語言defer預(yù)計算參數(shù)
Go 語言中所有的函數(shù)調(diào)用都是傳值的,雖然 defer 是關(guān)鍵字,但是也繼承了這個特性。假設(shè)我們想要計算 main 函數(shù)運(yùn)行的時間,可能會寫出以下的代碼:
結(jié)果是:
運(yùn)行結(jié)果并不符合我們的預(yù)期,這個現(xiàn)象背后的原因是什么呢?經(jīng)過分析,我們會發(fā)現(xiàn)調(diào)用 defer 關(guān)鍵字會立刻拷貝函數(shù)中引用的外部參數(shù),所以 time.Since(startedAt) 的結(jié)果不是在 main 函數(shù)退出之前計算的,而是在 defer 關(guān)鍵字調(diào)用時計算的【defer入棧的時候】,最終導(dǎo)致上述代碼輸出 0s
我們再來看個簡單例子來說明上述解釋:
當(dāng)代碼運(yùn)行到defer fmt.Println(test(i))的時候,會把defer右邊最外層函數(shù)的參數(shù)計算完畢,并傳遞進(jìn)函數(shù)里,但不會執(zhí)行函數(shù)體的代碼直到包裹defer的函數(shù)返回。我們先看會把defer右邊最外層函數(shù)的參數(shù)計算完畢,并傳遞進(jìn)函數(shù)里這句話,對應(yīng)例子就是先把test(i)算出來,此時i=1,計算test(1)得2,然后fmt.Println(2)入棧,等到最后程序運(yùn)行完了再運(yùn)行defer結(jié)果就是2(但不會執(zhí)行函數(shù)體的代碼直到包裹defer的函數(shù)返回)。
我們再來看一個例子與匿名函數(shù)結(jié)合:
結(jié)果:
使用匿名函數(shù),結(jié)果是101,相當(dāng)于i給到test方法的是100,那為什么呢?還是那句話:但不會執(zhí)行函數(shù)體的代碼直到包裹defer的函數(shù)返回
也就是說他會把整個{ fmt.Println(test(i)) }()函數(shù)體入棧,等到最后程序運(yùn)行完了再運(yùn)行defer,此時的i是100,運(yùn)行test后就是101了。
所以你要解決第一個打印為0s的問題,你就可以使用匿名函數(shù)來解決,如下:
結(jié)果:
在以下這段代碼中,我們操作一個文件,無論成功與否都需要關(guān)閉文件句柄。這里在三處不同的位置都調(diào)用了file.Close()方法,代碼顯得非常冗余。
我們利用延遲調(diào)用來優(yōu)化代碼。定義后的defer代碼,會在return之前返回,讓代碼顯得更加緊湊,且可讀性變強(qiáng),對上面的代碼改造如下:
我們通過這個示例來看一下延遲調(diào)用與正常代碼之間的執(zhí)行順序
先簡單分析一下代碼邏輯:
從輸出中,我們可以觀察到如下現(xiàn)象:
從這個實例中,我們很明顯觀察到,defer語句是在return之前執(zhí)行
如果一個函數(shù)內(nèi)定義了多個defer,則調(diào)用順序為LIFO(后進(jìn)先出)方式執(zhí)行。
仍然是相同的例子,但是在TestDefer中我們定義了三個defer輸出,根據(jù)LIFO原則,輸出的順序是3rd-2nd-1st,根據(jù)最后的結(jié)果,也是逆向向上執(zhí)行defer輸出。
就在整理這篇筆記的時候,發(fā)現(xiàn)了自己的認(rèn)知誤區(qū),主要是本節(jié)實例三中發(fā)現(xiàn)的,先來看一下英文的描述:
對于上面的這段話的理解:
下面是代碼執(zhí)行輸出,我們來一起分析一下:
雖然在a()函數(shù)內(nèi),顯示的返回了10,但是main函數(shù)中得到的結(jié)果是defer函數(shù)自增后的結(jié)果,我們來分析一下代碼:
在這篇文章的上一版,我曾經(jīng)嘗試用指針取解釋defer修改返回值的類型,但是感覺不夠透徹,也讓閱讀者非常困惑,索性參考了一下go官方blog中的一篇文章,在此基礎(chǔ)上進(jìn)行了擴(kuò)展。如需要閱讀原文,可以參考下面的文章。