[TOC]
golang語言defer特性詳解
defer語句是go語言提供的一種用于注冊延遲調(diào)用的機(jī)制,它可以讓函數(shù)在當(dāng)前函數(shù)執(zhí)行完畢后執(zhí)行,是go語言中一種很有用的特性。由于它使用起來簡單又方便,所以深得go語言開發(fā)者的歡迎。但是,真正想要使用好這一特性,卻得對(duì)這一特性深入理解它的原理,不然很容易掉進(jìn)一些奇怪的坑里還找不到原因。接下來,我們將一起來探討defer的使用方式,使用場景及一些容易產(chǎn)生誤解、混淆的規(guī)則。
什么是defer
首先我們來看下defer語句的官方解釋
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
defer語句注冊了一個(gè)函數(shù)調(diào)用,這個(gè)調(diào)用會(huì)延遲到defer語句所在的函數(shù)執(zhí)行完畢后執(zhí)行,所謂執(zhí)行完畢是指該函數(shù)執(zhí)行了return語句、函數(shù)體已執(zhí)行完最后一條語句或函數(shù)所在協(xié)程發(fā)生了恐慌。
如何使用defer
defer的使用方式很簡單,只需要在一個(gè)正常函數(shù)調(diào)用前面加上defer關(guān)鍵字即可(類似起協(xié)程時(shí)的go關(guān)鍵字),defer后面的函數(shù)調(diào)用會(huì)在defer所在的函數(shù)執(zhí)行完畢后執(zhí)行, 需要注意的是defer后面只能是函數(shù)調(diào)用,不能是表達(dá)式如 a++
等。
//demo1 defer使用實(shí)例
func main() {
fmt.Println("test1")
defer fmt.Println("defer")
fmt.Println("test2")
}
如demo1所示,我們先按正常函數(shù)調(diào)用調(diào)用了一行打印"test1",隨后用defer關(guān)鍵字編寫了一個(gè)延遲調(diào)用,打印“defer”, 最后再編寫了一個(gè)正常函數(shù)調(diào)用語句打印"test2",通過三個(gè)打印的輸出順序來簡單看一下defer的執(zhí)行時(shí)機(jī),運(yùn)行結(jié)果如下圖所示
從demo1的運(yùn)行結(jié)果我們可以看到,輸出順序是 "test1","test2","defer",因此,雖然fmt.Println(defer)
函數(shù)調(diào)用語句出現(xiàn)的早于fmt.Println(test2)
,但由于其前面加了defer關(guān)鍵字,延遲到了最后test2打印執(zhí)行完了才真正執(zhí)行函數(shù)調(diào)用
為什么需要defer
我們在寫代碼的時(shí)候,經(jīng)常會(huì)需要申請(qǐng)一些資源,比如申請(qǐng)可用數(shù)據(jù)庫連接、打開文件句柄、申請(qǐng)鎖、獲取可用網(wǎng)絡(luò)連接、申請(qǐng)內(nèi)存空間等,這些資源都有一個(gè)共同點(diǎn)那就是在我們使用完之后都需要將其釋放掉,否則會(huì)造成內(nèi)存泄漏或死鎖等其它問題。但由于開發(fā)人員一時(shí)疏忽忘記釋放資源是常有的事。此外,對(duì)于一些出口比較多的函數(shù),需要在每個(gè)出口處都重復(fù)的編寫資源釋放代碼,既容易造成遺漏,也導(dǎo)致很多重復(fù)代碼,代碼不夠簡潔。
golang直接在語言層面提供defer關(guān)鍵字來解決上述問題。當(dāng)我們成功申請(qǐng)了一項(xiàng)資源后,馬上使用defer語句注冊資源的釋放操作,在函數(shù)運(yùn)行完畢后,就會(huì)自動(dòng)執(zhí)行這些操作釋放資源,可以極大程度的避免了對(duì)資源釋放的遺忘。此外,對(duì)于出口較多的函數(shù),也無需在每個(gè)出口處再去編寫釋放資源的代碼。如示例demo2是一個(gè)打開文件獲得句柄處理文件再關(guān)閉文件的操作
//demo2 通過緊跟著資源申請(qǐng)代碼的defer來保證資源得到釋放
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close() //釋放資源
/*
讀取和處理文件內(nèi)容
*/
當(dāng)打開文件成功獲得文件句柄資源后,馬上通過defer定義一個(gè)釋放資源的延遲調(diào)用f.Close()
,避免后續(xù)忘記釋放資源,然后再編寫實(shí)際文件內(nèi)容處理的代碼。
因此,在諸如打開連接/關(guān)閉連接;申請(qǐng)/釋放鎖;打開文件/關(guān)閉文件等成對(duì)出現(xiàn)的操作場景里,defer會(huì)顯得格外方便和適用。
defer使用規(guī)則
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.
上面這段是官方對(duì)defer使用機(jī)制的描述,大概意思是:每次defer語句執(zhí)行時(shí),會(huì)將defer定義的函數(shù)以及函數(shù)參數(shù)拷貝出來壓到一個(gè)專門的defer函數(shù)棧中,此時(shí),函數(shù)并不會(huì)真正執(zhí)行;當(dāng)在外層函數(shù)退出之前,defer函數(shù)會(huì)按照定義的順序逆序執(zhí)行;如果defer要執(zhí)行的函數(shù)為nil,會(huì)在函數(shù)退出之前defer函數(shù)真正執(zhí)行時(shí)panic,而不是在defer語句執(zhí)行時(shí)。
文字不長,理解起來似乎也不難,但是如果不深入了解,坑卻不少,總結(jié)下來大概有這么幾個(gè)要注意的地方:
- 一個(gè)函數(shù)中有多個(gè)defer時(shí)的運(yùn)行順序
- defer語句執(zhí)行時(shí)的拷貝機(jī)制
- defer如何影響函數(shù)返回值
此外,還有一些在這段話里沒有講述的如defer與閉包、defer和panic等知識(shí)點(diǎn)。接下里,我們會(huì)挨個(gè)分析一下。
多個(gè)defer運(yùn)行順序
當(dāng)一個(gè)函數(shù)中含有多個(gè)defer語句時(shí),函數(shù)return前會(huì)按defer定義的順序逆序執(zhí)行,先進(jìn)后出,也就是說最先注冊的defer函數(shù)調(diào)用最后執(zhí)行。這一機(jī)制也好理解,后申請(qǐng)的資源有可能對(duì)前面申請(qǐng)的資源有依賴,如果將先申請(qǐng)的資源直接釋放掉了可能會(huì)導(dǎo)致后申請(qǐng)的資源釋放時(shí)各種異常。我們可以通過一個(gè)例子來驗(yàn)證一下執(zhí)行順序。
//demo3 多個(gè)defer執(zhí)行順序
func main() {
fmt.Println("test1")
defer fmt.Println("defer1")
fmt.Println("test2")
defer fmt.Println("defer2")
fmt.Println("test3")
}
運(yùn)行結(jié)果如下圖所示:
當(dāng)執(zhí)行完函數(shù)正常執(zhí)行語句test1,test2和test3的打印后,先執(zhí)行了后定義的延遲調(diào)用fmt.Println("defer2")
,最后執(zhí)行了最先定義的延遲調(diào)用fmt.Println("defer1")
defer語句執(zhí)行時(shí)的拷貝機(jī)制
經(jīng)過前文的講述,我們知道,當(dāng)我們執(zhí)行defer語句時(shí),函數(shù)調(diào)用不會(huì)馬上發(fā)生,語言層面會(huì)先把defer注冊的函數(shù)及變量拷貝到defer棧中保存,直到函數(shù)return前才執(zhí)行defer中的函數(shù)調(diào)用。需要格外注意的是,這一拷貝拷貝的是那一刻函數(shù)的值和參數(shù)的值。注冊之后再修改函數(shù)值或參數(shù)值時(shí),不會(huì)生效。接下來我們同樣用代碼說話:
//demo4 defer函數(shù)在defer語句執(zhí)行那一刻就已經(jīng)確定
func main() {
test := func() {
fmt.Println("I am function test1")
}
defer test()
test = func() {
fmt.Println("I am function test2")
}
}
運(yùn)行結(jié)果如下圖所示:
在demo4中,我們定義了一個(gè)函數(shù)變量test,然后將test調(diào)用添加為一個(gè)延遲調(diào)用,隨后,修改test的值,defer雖然是最后運(yùn)行,但是從結(jié)果中我們可以看到,執(zhí)行的依舊是defer注冊時(shí)那一刻test對(duì)應(yīng)的函數(shù)調(diào)用,也即是打印了test1的函數(shù)調(diào)用。
函數(shù)參數(shù)也是同樣的道理,接下來我們看一個(gè)函數(shù)參數(shù)的例子
//demo5 defer函數(shù)參數(shù)的值在注冊那一刻就已經(jīng)確定
func f5() {
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++
}
可以看到,執(zhí)行的輸出的是10而不是11。這也是同樣的道理,在使用defer注冊延遲函數(shù)那一刻,函數(shù)參數(shù)的值已經(jīng)確定是10,后續(xù)x的變化不會(huì)影響到已經(jīng)拷貝儲(chǔ)存好的函數(shù)參數(shù)。
到這里,拷貝規(guī)則似乎很明確了,然而,我們再來看看以下兩個(gè)demo,讀者可以在看結(jié)果之前,自己先想一下輸出結(jié)果。
//demo6 defer 函數(shù)傳遞參數(shù)為指針傳遞
func main() {
x := 10
defer func(a *int) {
fmt.Println(*a)
}(&x)
x++
}
//demo7 defer 延遲函數(shù)為閉包
func main() {
x := 10
defer func() {
fmt.Println(x)
}()
x++
}
運(yùn)行結(jié)果為:
demo6:
demo7:
很多人可能會(huì)覺得應(yīng)該輸出10,然而運(yùn)行下來兩個(gè)程序最后輸出的結(jié)果都為11而不是10,這是怎么回事呢,不是說函數(shù)調(diào)用都已經(jīng)在defer語句執(zhí)行時(shí)就已經(jīng)確認(rèn)了嗎,怎么最后輸出的結(jié)果都為11而不是10呢,是這個(gè)規(guī)則是錯(cuò)的嗎?其實(shí)并不是,我們來具體分析一下。
在demo6中,與demo5的區(qū)別在于,demo5傳遞的是一個(gè)int型的值,而demo6傳遞的是一個(gè)int型的指針,那我們按照拷貝規(guī)則想一下,在defer語句執(zhí)行時(shí),函數(shù)參數(shù)實(shí)際上傳遞的是一個(gè)指針,指向變量x的地址,當(dāng)函數(shù)return之前defer定義的函數(shù)調(diào)用執(zhí)行時(shí),該指針指向的地址對(duì)應(yīng)的值即x已經(jīng)變成了11,所以打印11是正常的,也并沒有違反該拷貝規(guī)則。
demo7與demo5、demo6稍稍有些不一樣,demo7的x并不是通過函數(shù)調(diào)用的參數(shù)傳進(jìn)去的,而是一個(gè)閉包,閉包里的變量本質(zhì)上是對(duì)上層變量的引用,因此最后的值就是引用的值,也可以說,defer函數(shù)閉包變量的值實(shí)際上到最后執(zhí)行時(shí),才最終確認(rèn)是多少,因此與前面的拷貝規(guī)則也并不沖突,我們可以通過如下demo做個(gè)驗(yàn)證,即將兩處x的地址打印出來看是否一致
//demo8 defer 閉包驗(yàn)證
func main() {
x := 10
fmt.Printf("normal:%p\n", &x)
defer func() {
fmt.Printf("defer:%p\n", &x)
fmt.Println(x)
}()
x++
}
運(yùn)行結(jié)果如下:
地址一致,可證實(shí)閉包里和外層引用了同一塊內(nèi)存空間,外層的改變會(huì)影響到閉包里面值的改變
defer和函數(shù)返回值
從官方話術(shù)中我們可以知道defer發(fā)生的時(shí)機(jī)是在函數(shù)執(zhí)行return語句之后,既然在return之后,是不是意味著我們可以利用defer來對(duì)函數(shù)的返回值做一些事情呢,那么什么情況下defer會(huì)影響到函數(shù)返回值,什么時(shí)候不會(huì)影響呢?
defer和非命名返回值
我們先來看以下兩個(gè)例子
//demo10 defer函數(shù)與非命名返回值之間的關(guān)系
func f10() int {
x := 10
defer func() {
x++
}()
return x
}
//demo11 defer函數(shù)與非命名返回值之間的關(guān)系
func f11() *int {
a := 10
b := &a
defer func() {
*b++
}()
return b
}
func main() {
fmt.Println("f10", f10())
fmt.Println("f11", *f11())
}
我們可以推算一下結(jié)果,然后再實(shí)際運(yùn)行一下看結(jié)果和自己所想是否一致,在本demo中,f10和f11執(zhí)行的結(jié)果如下圖所示
f10中,延遲函數(shù)的調(diào)用并沒有影響到返回值,f11中,延遲函數(shù)的調(diào)用成功"影響"到了返回值, 這個(gè)怎么來理解呢。其實(shí)我們可以對(duì)函數(shù)返回進(jìn)行"拆解","拆解"后的代碼如下所示:
//demo10_1 defer函數(shù)與非命名返回值之間的關(guān)系, return拆解
func f10_1() int {
x := 10
defer func() {
x++
}()
//return x => 拆解
_result := x
return _result //實(shí)際返回的是_result的值,因此defer中修改x的值對(duì)返回值沒有影響
}
//demo11_1 defer函數(shù)與返回值之間的關(guān)系, return拆解
func f11_1() *int {
a := 10
b := &a
defer func() {
*b++
}()
//return b => 拆解
_result := b
return _result //執(zhí)行defer函數(shù)調(diào)用*b++, 修改了b指向的內(nèi)存空間的值,實(shí)際返回的是result指針
}
注:拆解成這樣只是為了方便理解,拆解后的代碼會(huì)更加清晰,函數(shù)返回值的變化也更加直觀,當(dāng)我們無法判斷時(shí),就可以將return操作一分為二,一部分是計(jì)算返回值,一部分是真正的返回,再去判斷就不容易出錯(cuò)了。各部分執(zhí)行順序如下
- 計(jì)算返回值
- 執(zhí)行defer函數(shù)調(diào)用
- 函數(shù)返回第一步中計(jì)算的返回值
因此,實(shí)際上在這種模式中,defer無法實(shí)際影響到函數(shù)的返回值,對(duì)于f11中,函數(shù)返回的指針的值并沒有變化,受影響的只是該指針指向的區(qū)域?qū)?yīng)的值,可以說時(shí)間接上改變了返回值,跟普通函數(shù)傳入指針的做法沒什么區(qū)別。我們可以通過直接在defer函數(shù)調(diào)用中,改變b指針指向來證實(shí)這一規(guī)則。
//demo12 defer函數(shù)與非命名返回值之間的關(guān)系
func f12() *int {
a := 10
b := &a
fmt.Println("b", b)
defer func() {
c := 12
b = &c
fmt.Println("defer", b)
}()
return b
}
從輸出結(jié)果可以看到,雖然defer中把b的指針重新指向了值為12的c的地址,但是最終返回值并未改變。其實(shí)這一特性,與前文講述的defer的特性很像,函數(shù)內(nèi)容都是在return/defer語句執(zhí)行那一刻就已經(jīng)確定,延遲函數(shù)調(diào)用并不會(huì)改變返回值/參數(shù)值/函數(shù)值
defer和命名返回值
那么,defer就真的無法影響函數(shù)的返回值了嗎?其實(shí)也不然。在go語言中,一個(gè)函數(shù)返回返回值有兩種形式,除了前面講的那種之外,還有一種返回形式叫命名返回值,那么,在命名返回值中,defer會(huì)是什么效果呢。我們依舊通過代碼來看一下。
//demo13 defer函數(shù)與命名返回值之間的關(guān)系
func f13() (result int) {
defer func() {
result++
}()
return 10
}
//demo14 defer函數(shù)與命名返回值之間的關(guān)系
func f14() (result int) {
result = 10
defer func() {
result++
}()
return result
}
//demo15 defer函數(shù)與命名返回值之間的關(guān)系
func f15() (b *int) {
a := 10
b = &a
fmt.Println("b", b)
defer func() {
c := 12
b = &c
fmt.Println("defer", b)
}()
return
}
func main() {
fmt.Println("f13", f13())
fmt.Println("f14", f14())
t := f15()
fmt.Println("f15", *t, t)
}
執(zhí)行結(jié)果如下圖所示:
從結(jié)果可以看到,三個(gè)示例中返回值都被defer函數(shù)調(diào)用成功修改,我們同樣可以通過return拆解來理解這一現(xiàn)象。在前面的非命名函數(shù)中,最后一步我們可以拆解成有一個(gè)專門儲(chǔ)存函數(shù)返回值的臨時(shí)變量,最終函數(shù)返回的是該變量的值,因此defer函數(shù)對(duì)原有返回值的修改無效,但是在命名返回方式中,最終函數(shù)返回的就是命名返回變量的值,因此,對(duì)該命名返回變量的修改會(huì)影響到最終的函數(shù)返回值
defer和recover
在go語言中,當(dāng)程序發(fā)生異常時(shí),一般我們可以選擇直接panic來讓程序停止運(yùn)行,但很多時(shí)候,在程序異常停止前,我們希望做一些“掃尾工作”。此外,對(duì)于服務(wù)端程序來說,很多異常情況是可以容忍程序繼續(xù)執(zhí)行的,并不希望程序因此宕掉,此時(shí)我們可能更希望捕獲異常然后通過異常的類型來判斷是否需要將程序從異常中恢復(fù)。因此,go語言提供了recover函數(shù)來進(jìn)行panic捕獲。由于程序任何位置都可能發(fā)生恐慌,因此,作為函數(shù)退出必定執(zhí)行的defer延遲調(diào)用里,是最適合捕獲panic的位置(我們前面提到,defer延遲調(diào)用執(zhí)行的時(shí)機(jī)之一就是發(fā)生panic時(shí)),所以,在go語言的設(shè)計(jì)里,recover只會(huì)在defer中生效,且此時(shí)defer延遲調(diào)用必須是匿名函數(shù),defer+recover起到了很多語言里面try...catch...的效果。同樣來看一個(gè)例子
//demo16 defer與recover
func f16() {
defer func() {
if err := recover(); err != nil {
fmt.Println("catch err:", err)
}
}()
panic(errors.New("TEST"))
}
func main() {
f16()
fmt.Println("I am OK.")
}
如果f16中沒有異常捕獲,panic會(huì)導(dǎo)致整個(gè)程序直接退出,fmt.Println("I am OK.")
這一語句將無法執(zhí)行,當(dāng)我們在defer中將入了recover異常捕獲后,執(zhí)行結(jié)果如下圖所示
程序成功捕獲異常并從異常中恢復(fù)成功的繼續(xù)往下執(zhí)行打印出了"I am OK."。最后有興趣的讀者可以再試想以下,如果defer中發(fā)生異常,又會(huì)發(fā)生什么事呢?這個(gè)由于不算defer的知識(shí)點(diǎn),我們就不在此驗(yàn)證了。
總結(jié)
在本文中,我們主要通過一些示例探討了如下一些defer的特性
defer本質(zhì)上是注冊了一個(gè)延時(shí)函數(shù),當(dāng)defer語句所在上下文函數(shù)執(zhí)行完畢后再進(jìn)行延遲函數(shù)的實(shí)際調(diào)用
defer函數(shù)及對(duì)應(yīng)參數(shù)在defer語句執(zhí)行時(shí)就已經(jīng)確定,只不過將函數(shù)執(zhí)行延后
當(dāng)存在多個(gè)defer時(shí),依照defer語句執(zhí)行的先后順序,逆序進(jìn)行延遲函數(shù)調(diào)用
defer和閉包一起用時(shí),閉包變量的值在函數(shù)調(diào)用執(zhí)行時(shí)才最終確定
對(duì)于非命名返回值函數(shù),defer無法修改返回值,但對(duì)于命名返回值函數(shù),可以通過defer來修改函數(shù)的返回值,因此,當(dāng)我們想通過defer來靈活操作函數(shù)返回值時(shí),可使用命名返回值方式
defer + recover 有點(diǎn)類似于其它語言的try…catch,recover只在defer延遲函數(shù)調(diào)用里才能生效
defer是go語言中極其實(shí)用又方便的特性,使用好了可以使程序更加安全,讓代碼簡潔又優(yōu)雅,但前提是對(duì)其本身的特性掌握透徹。只要我們對(duì)本文中這些規(guī)則都理解清楚了,相信可以在defer的使用上更加得心應(yīng)手。