Go
語言中有個 defer
關鍵字,經常使用於實現延遲函數來保證關鍵代碼的最終執行,常言道: "未雨綢繆方可有備無患".html
延遲函數就是這麼一種機制,不管程序是正常返回仍是異常報錯,只要存在延遲函數都能保證這部分關鍵邏輯最終執行,因此用來作些資源清理等操做再合適不過了.golang
平常開發編程中,有些操做老是成雙成對出現的,有開始就有結束,有打開就要關閉,還有一些連續依賴關係等等.編程
通常來講,咱們須要控制結束語句,在合適的位置和時機控制結束語句,手動保證整個程序善始善終,不遺漏清理收尾操做.c#
最多見的拷貝文件操做大體流程以下:微信
srcFile, err := os.Open("fib.txt")
if err != nil {
t.Error(err)
return
}
複製代碼
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
t.Error(err)
return
}
複製代碼
io.Copy(dstFile, srcFile)
複製代碼
dstFile.Close()
srcFile.Close()
複製代碼
srcFile.Close()
複製代碼
值得注意的是: 這種拷貝文件的操做須要特別注意操做順序並且也不要忘記釋放資源,好比先打開再關閉等等!數據結構
func TestCopyFileWithoutDefer(t *testing.T) {
srcFile, err := os.Open("fib.txt")
if err != nil {
t.Error(err)
return
}
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
t.Error(err)
return
}
io.Copy(dstFile, srcFile)
dstFile.Close()
srcFile.Close()
}
複製代碼
「雪之夢技術驛站」: 上述代碼邏輯仍是清晰簡單的,可能不會忘記釋放資源也能保證操做順序,可是若是邏輯代碼比較複雜的狀況,這時候就有必定的實現難度了!函數
多是爲了簡化相似代碼的邏輯,Go
語言引入了 defer
關鍵字,創造了"延遲函數"的概念.測試
defer
的文件拷貝func TestCopyFileWithoutDefer(t *testing.T) {
if srcFile, err := os.Open("fib.txt"); err != nil {
t.Error(err)
return
} else {
if dstFile,err := os.Create("fib.txt.bak");err != nil{
t.Error(err)
return
}else{
io.Copy(dstFile,srcFile)
dstFile.Close()
srcFile.Close()
}
}
}
複製代碼
defer
的文件拷貝func TestCopyFileWithDefer(t *testing.T) {
if srcFile, err := os.Open("fib.txt"); err != nil {
t.Error(err)
return
} else {
defer srcFile.Close()
if dstFile, err := os.Create("fib.txt.bak"); err != nil {
t.Error(err)
return
} else {
defer dstFile.Close()
io.Copy(dstFile, srcFile)
}
}
}
複製代碼
上述示例代碼簡單展現了 defer
關鍵字的基本使用方式,顯著的好處在於 Open/Close
是一對操做,不會由於寫到最後而忘記 Close
操做,並且連續依賴時也能正常保證延遲時機.ui
簡而言之,若是函數內部存在連續依賴關係,也就是說建立順序是 A->B->C
而銷燬順序是 C->B->A
.這時候使用 defer
關鍵字最合適不過.google
官方文檔相關表述見 Defer statements
若是沒有 defer
延遲函數前,普通函數正常運行:
func TestFuncWithoutDefer(t *testing.T) {
// 「雪之夢技術驛站」: 正常順序
t.Log("「雪之夢技術驛站」: 正常順序")
// 1 2
t.Log(1)
t.Log(2)
}
複製代碼
當添加 defer
關鍵字實現延遲後,原來的 1
被推遲到 2
後面而不是以前的 1 2
順序.
func TestFuncWithDefer(t *testing.T) {
// 「雪之夢技術驛站」: 正常順序執行完畢後才執行 defer 代碼
t.Log(" 「雪之夢技術驛站」: 正常順序執行完畢後才執行 defer 代碼")
// 2 1
defer t.Log(1)
t.Log(2)
}
複製代碼
若是存在多個 defer
關鍵字,執行順序可想而知,越日後的越先執行,這樣才能保證按照依賴順序依次釋放資源.
func TestFuncWithMultipleDefer(t *testing.T) {
// 「雪之夢技術驛站」: 猜想 defer 底層實現數據結構多是棧,先進後出.
t.Log(" 「雪之夢技術驛站」: 猜想 defer 底層實現數據結構多是棧,先進後出.")
// 3 2 1
defer t.Log(1)
defer t.Log(2)
t.Log(3)
}
複製代碼
相信你已經明白了多個 defer
語句的執行順序,那就測試一下吧!
func TestFuncWithMultipleDeferOrder(t *testing.T) {
// 「雪之夢技術驛站」: defer 底層實現數據結構相似於棧結構,依次倒敘執行多個 defer 語句
t.Log(" 「雪之夢技術驛站」: defer 底層實現數據結構相似於棧結構,依次倒敘執行多個 defer 語句")
// 2 3 1
defer t.Log(1)
t.Log(2)
defer t.Log(3)
}
複製代碼
初步認識了 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"語句調用一個函數,該函數的執行被推遲到周圍函數返回的那一刻,這是由於周圍函數執行了一個return語句,到達了函數體的末尾,或者是由於相應的協程正在驚慌.
具體來講,延遲函數的執行時機大概分爲三種狀況:
because the surrounding function executed a return statement
return
後面的 t.Log(4)
語句天然是不會運行的,程序最終輸出結果爲 3 2 1
說明了 defer
語句會在周圍函數執行 return
前依次逆序執行.
func funcWithMultipleDeferAndReturn() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
return
fmt.Println(4)
}
func TestFuncWithMultipleDeferAndReturn(t *testing.T) {
// 「雪之夢技術驛站」: defer 延遲函數會在包圍函數正常return以前逆序執行.
t.Log(" 「雪之夢技術驛站」: defer 延遲函數會在包圍函數正常return以前逆序執行.")
// 3 2 1
funcWithMultipleDeferAndReturn()
}
複製代碼
reached the end of its function body
周圍函數的函數體運行到結尾前逆序執行多個 defer
語句,即先輸出 3
後依次輸出 2 1
. 最終函數的輸出結果是 3 2 1
,也就說是沒有 return
聲明也能保證結束前執行完 defer
延遲函數.
func funcWithMultipleDeferAndEnd() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
}
func TestFuncWithMultipleDeferAndEnd(t *testing.T) {
// 「雪之夢技術驛站」: defer 延遲函數會在包圍函數到達函數體結尾以前逆序執行.
t.Log(" 「雪之夢技術驛站」: defer 延遲函數會在包圍函數到達函數體結尾以前逆序執行.")
// 3 2 1
funcWithMultipleDeferAndEnd()
}
複製代碼
because the corresponding goroutine is panicking
周圍函數萬一發生 panic
時也會先運行前面已經定義好的 defer
語句,而 panic
後續代碼由於沒有特殊處理,因此程序崩潰了也就沒法運行.
函數的最終輸出結果是 3 2 1 panic
,如此看來 defer
延遲函數仍是很是盡忠職守的,雖然內心很慌但仍是能保證老弱病殘先行撤退!
func funcWithMultipleDeferAndPanic() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
panic("panic")
fmt.Println(4)
}
func TestFuncWithMultipleDeferAndPanic(t *testing.T) {
// 「雪之夢技術驛站」: defer 延遲函數會在包圍函數panic惶恐不安以前逆序執行.
t.Log(" 「雪之夢技術驛站」: defer 延遲函數會在包圍函數panic惶恐不安以前逆序執行.")
// 3 2 1
funcWithMultipleDeferAndPanic()
}
複製代碼
經過解讀 defer
延遲函數的定義以及相關示例,相信已經講清楚什麼是 defer
延遲函數了吧?
簡單地說,延遲函數就是一種未雨綢繆的規劃機制,幫助開發者編程程序時及時作好收尾善後工做,提早作好預案以準備隨時應對各類狀況.
panic
惶恐不安時,程序存在延遲函數也不會忘記執行,提早作好預案發揮了做用.因此不管是正常運行仍是異常運行,提早作好預案老是沒錯的,基本上能夠保證萬無一失,因此不妨考慮考慮 defer
延遲函數?
基本上成雙成對的操做均可以使用延遲函數,尤爲是申請的資源先後存在依賴關係時更應該使用 defer
關鍵字來簡化處理邏輯.
下面舉兩個常見例子來講明延遲函數的應用場景.
文件操做通常會涉及到打開和開閉操做,尤爲是文件之間拷貝操做更是有着嚴格的順序,只須要按照申請資源的順序緊跟着defer
就能夠知足資源釋放操做.
func readFileWithDefer(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
複製代碼
鎖的申請和釋放是保證同步的一種重要機制,須要申請多個鎖資源時可能存在依賴關係,不妨嘗試一下延遲函數!
var mu sync.Mutex
var m = make(map[string]int)
func lookupWithDefer(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
複製代碼
defer
延遲函數是保障關鍵邏輯正常運行的一種機制,若是存在多個延遲函數的話,通常會按照逆序的順序運行,相似於棧結構.
延遲函數的運行時機通常有三種狀況:
func funcWithMultipleDeferAndReturn() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
return
fmt.Println(4)
}
複製代碼
func funcWithMultipleDeferAndEnd() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
}
複製代碼
func funcWithMultipleDeferAndPanic() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
panic("panic")
fmt.Println(4)
}
複製代碼
本文主要介紹了什麼是 defer
延遲函數,經過解讀官方文檔並配套相關代碼認識了延遲函數,可是延遲函數中存在一些可能使人比較迷惑的地方.
讀者不妨看一下下面的代碼,將內心的猜測和實際運行結果比較一下,咱們下次再接着分享,感謝你的閱讀.
func deferFuncWithAnonymousReturnValue() int {
var retVal int
defer func() {
retVal++
}()
return 0
}
func deferFuncWithNamedReturnValue() (retVal int) {
defer func() {
retVal++
}()
return 0
}
複製代碼
若是本文對你有所幫助,不用讚揚,點贊鼓勵一下就是最大的承認,順便也能夠關注下微信公衆號「 雪之夢技術驛站 」喲!