go 學習筆記之解讀什麼是defer延遲函式
Go
語言中有個 defer
關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患".
延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了.
出入成雙有始有終
日常開發程式設計中,有些操作總是成雙成對出現的,有開始就有結束,有開啟就要關閉,還有一些連續依賴關係等等.
一般來說,我們需要控制結束語句,在合適的位置和時機控制結束語句,手動保證整個程式有始有終,不遺漏清理收尾操作.
最常見的拷貝檔案操作大致流程如下:
- 開啟原始檔
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
操作,而且連續依賴時也能正常保證延遲時機.
簡而言之,如果函式內部存在連續依賴關係,也就是說建立順序是 A->B->C
而銷燬順序是 C->B->A
.這時候使用 defer
關鍵字最合適不過.
懶人福音延遲函式
官方文件相關表述見 Defer statements[1]
如果沒有 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語句,到達了函式體的末尾,或者是因為相應的協程正在驚慌.
具體來說,延遲函式的執行時機大概分為三種情況:
周圍函式執行 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
延遲函式了吧?
簡單地說,延遲函式就是一種未雨綢繆的規劃機制,幫助開發者程式設計程式時及時做好收尾善後工作,提前做好預案以準備隨時應對各種情況.
- 當週圍函式正常執行到到達函式體結尾時,如果發現存在延遲函式自然會逆序執行延遲函式.
- 當週圍函式正常執行遇到 return 語句準備返回給呼叫者時,存在延遲函式時也會執行,同樣滿足善後清理的需求.
- 當週圍函式異常執行不小心
panic
驚慌失措時,程式存在延遲函式也不會忘記執行,提前做好預案發揮了作用.
所以不論是正常執行還是異常執行,提前做好預案總是沒錯的,基本上可以保證萬無一失,所以不妨考慮考慮 defer
延遲函式?
延遲函式應用場景
基本上成雙成對的操作都可以使用延遲函式,尤其是申請的資源前後存在依賴關係時更應該使用 defer
關鍵字來簡化處理邏輯.
下面舉兩個常見例子來說明延遲函式的應用場景.
- Open/Close
檔案操作一般會涉及到開啟和開閉操作,尤其是檔案之間拷貝操作更是有著嚴格的順序,只需要按照申請資源的順序緊跟著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)
}
- Lock/Unlock
鎖的申請和釋放是保證同步的一種重要機制,需要申請多個鎖資源時可能存在依賴關係,不妨嘗試一下延遲函式!
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
}
延伸閱讀參考文件
- Defer_statements[2]
- go 語言的 defer 語句[3]
- Go defer 實現原理剖析[4]
- go 語言 defer 你不知道的祕密![5]
- Go 語言中 defer 的一些坑[6]
- go defer (go 延遲函式)[7]
如果本文對你有所幫助,不用讚賞,點贊鼓勵一下就是最大的認可,順便也可以關注下微信公眾號「 雪之夢技術驛站 」喲!
參考資料
Defer statements: https://golang.google.cn/ref/spec#Defer_statements
Defer_statements: https://golang.google.cn/ref/spec#Defer_statements
go語言的defer語句: https://www.jianshu.com/p/5b0b36f398a2
Go defer實現原理剖析: https://studygolang.com/articles/16067
go語言 defer 你不知道的祕密!: https://www.cnblogs.com/baizx/p/5024547.html
Go語言中defer的一些坑: https://www.jianshu.com/p/79c029c0bd58
go defer (go延遲函式): https://www.cnblogs.com/ysherlock/p/8150726.html
相關文章
- go 學習筆記之咬文嚼字帶你弄清楚什麼是 defer延遲函式Go筆記函式
- go defer 學習筆記Go筆記
- 兄弟連go教程(15)函式 - 延遲呼叫Go函式
- 【Go】Go語言學習筆記-2-函式Go筆記函式
- 函式 - Go 學習記錄函式Go
- pandas之常用基本函式學習筆記函式筆記
- go 學習筆記之學習函數語言程式設計前不要忘了函式基礎Go筆記函數程式設計函式
- 什麼是延遲?怎樣解決?—Vecloud微雲Cloud
- day10學習筆記之函式上筆記函式
- hive學習筆記之七:內建函式Hive筆記函式
- 生成函式 學習筆記函式筆記
- async函式學習筆記。函式筆記
- Golang研學:defer!如何掌握並用好(延遲執行)Golang
- MYSQL學習筆記14: 函式MySql筆記函式
- python學習筆記(六)——函式Python筆記函式
- TS學習筆記(四):函式筆記函式
- Golang學習筆記-1.6 函式Golang筆記函式
- JavaScript學習筆記 - 原生函式JavaScript筆記函式
- Oracle學習筆記(6)——函式Oracle筆記函式
- 什麼是高延遲檔案傳輸?為什麼要使用高延遲檔案傳輸
- 深度學習——loss函式的學習筆記深度學習函式筆記
- C++之類解構函式為什麼是虛擬函式C++函式
- Go學習筆記-GMP詳解Go筆記
- 【opencv學習筆記】027之直方圖反向投影 - calcBackProject函式詳解OpenCV筆記直方圖Project函式
- 《Haskell趣學指南》筆記之函式Haskell筆記函式
- 《Spring揭祕》學習筆記——IOC是什麼Spring筆記
- MYSQL學習筆記7: 聚合函式MySql筆記函式
- C++學習筆記(二)——函式C++筆記函式
- OpenCV學習筆記(4)——mixChannels函式OpenCV筆記函式
- OpenCV學習筆記(5)——normalize函式OpenCV筆記ORM函式
- Flutter學習筆記(4)--Dart函式Flutter筆記Dart函式
- c語言學習筆記===函式C語言筆記函式
- js純函式學習筆記(一)JS函式筆記
- go 學習筆記之工作空間Go筆記
- 《Go 語言程式設計》讀書筆記 (二)函式Go程式設計筆記函式
- web前端教程之JavaScript學習筆記之遞迴函式Web前端JavaScript筆記遞迴函式
- 讀材料後筆記:什麼是DevOps筆記dev
- Go 學習筆記Go筆記