go 學習筆記之解讀什麼是defer延遲函數

2020-12-25 雪之夢技術驛站

Go 語言中有個 defer 關鍵字,常用於實現延遲函數來保證關鍵代碼的最終執行,常言道: "未雨綢繆方可有備無患".

延遲函數就是這麼一種機制,無論程序是正常返回還是異常報錯,只要存在延遲函數都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了.

出入成雙有始有終

日常開發編程中,有些操作總是成雙成對出現的,有開始就有結束,有打開就要關閉,還有一些連續依賴關係等等.

一般來說,我們需要控制結束語句,在合適的位置和時機控制結束語句,手動保證整個程序有始有終,不遺漏清理收尾操作.

最常見的拷貝文件操作大致流程如下:

打開源文件

創建目標文件

拷貝源文件到目標文件io.Copy(dstFile, srcFile)關閉目標文件dstFile.Close()srcFile.Close()關閉源文件srcFile.Close()值得注意的是: 這種拷貝文件的操作需要特別注意操作順序而且也不要忘記釋放資源,比如先打開再關閉等等!

「雪之夢技術驛站」: 上述代碼邏輯還是清晰簡單的,可能不會忘記釋放資源也能保證操作順序,但是如果邏輯代碼比較複雜的情況,這時候就有一定的實現難度了!

可能是為了簡化類似代碼的邏輯,Go 語言引入了 defer 關鍵字,創造了"延遲函數"的概念.

無 defer 的文件拷貝

有 defer 的文件拷貝

上述示例代碼簡單展示了 defer 關鍵字的基本使用方式,顯著的好處在於 Open/Close 是一對操作,不會因為寫到最後而忘記 Close 操作,而且連續依賴時也能正常保證延遲時機.

簡而言之,如果函數內部存在連續依賴關係,也就是說創建順序是 A->B->C 而銷毀順序是 C->B->A.這時候使用 defer 關鍵字最合適不過.

懶人福音延遲函數

官方文檔相關表述見 Defer statements[1]

如果沒有 defer 延遲函數前,普通函數正常運行:

當添加 defer 關鍵字實現延遲後,原來的 1 被推遲到 2 後面而不是之前的 1 2 順序.

如果存在多個 defer 關鍵字,執行順序可想而知,越往後的越先執行,這樣才能保證按照依賴順序依次釋放資源.

相信你已經明白了多個 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"語句調用一個函數,該函數的執行被推遲到周圍函數返回的那一刻,這是因為周圍函數執行了一個return語句,到達了函數體的末尾,或者是因為相應的協程正在驚慌.

具體來說,延遲函數的執行時機大概分為三種情況:

周圍函數執行 return

because the surrounding functionexecuted a return statement

return 後面的 t.Log(4) 語句自然是不會運行的,程序最終輸出結果為 3 2 1 說明了 defer 語句會在周圍函數執行 return 前依次逆序執行.

周圍函數到達函數體

reached the endof its function body

周圍函數的函數體運行到結尾前逆序執行多個 defer 語句,即先輸出 3 後依次輸出 2 1. 最終函數的輸出結果是 3 2 1 ,也就說是沒有 return 聲明也能保證結束前執行完 defer 延遲函數.

當前協程正驚慌失措

because the corresponding goroutine is panicking

周圍函數萬一發生 panic 時也會先運行前面已經定義好的 defer 語句,而 panic 後續代碼因為沒有特殊處理,所以程序崩潰了也就無法運行.

函數的最終輸出結果是 3 2 1 panic ,如此看來 defer 延遲函數還是非常盡忠職守的,雖然心裡很慌但還是能保證老弱病殘先行撤退!

通過解讀 defer 延遲函數的定義以及相關示例,相信已經講清楚什麼是 defer 延遲函數了吧?

簡單地說,延遲函數就是一種未雨綢繆的規劃機制,幫助開發者編程程序時及時做好收尾善後工作,提前做好預案以準備隨時應對各種情況.

當周圍函數正常執行到到達函數體結尾時,如果發現存在延遲函數自然會逆序執行延遲函數.當周圍函數正常執行遇到 return 語句準備返回給調用者時,存在延遲函數時也會執行,同樣滿足善後清理的需求.當周圍函數異常運行不小心 panic 驚慌失措時,程序存在延遲函數也不會忘記執行,提前做好預案發揮了作用.所以不論是正常運行還是異常運行,提前做好預案總是沒錯的,基本上可以保證萬無一失,所以不妨考慮考慮 defer 延遲函數?

go-error-about-lovely.png

延遲函數應用場景

基本上成雙成對的操作都可以使用延遲函數,尤其是申請的資源前後存在依賴關係時更應該使用 defer 關鍵字來簡化處理邏輯.

下面舉兩個常見例子來說明延遲函數的應用場景.

Open/Close文件操作一般會涉及到打開和開閉操作,尤其是文件之間拷貝操作更是有著嚴格的順序,只需要按照申請資源的順序緊跟著defer 就可以滿足資源釋放操作.

Lock/Unlock鎖的申請和釋放是保證同步的一種重要機制,需要申請多個鎖資源時可能存在依賴關係,不妨嘗試一下延遲函數!

總結以及下節預告

defer 延遲函數是保障關鍵邏輯正常運行的一種機制,如果存在多個延遲函數的話,一般會按照逆序的順序運行,類似於棧結構.

延遲函數的運行時機一般有三種情況:

周圍函數遇到返回時

周圍函數函數體結尾處

當前協程驚慌失措中

本文主要介紹了什麼是 defer 延遲函數,通過解讀官方文檔並配套相關代碼認識了延遲函數,但是延遲函數中存在一些可能令人比較迷惑的地方.

讀者不妨看一下下面的代碼,將心裡的猜想和實際運行結果比較一下,我們下次再接著分享,感謝你的閱讀.

延伸閱讀參考文檔

Defer_statements[2]go 語言的 defer 語句[3]Go defer 實現原理剖析[4]go 語言 defer 你不知道的秘密![5]Go 語言中 defer 的一些坑[6]go defer (go 延遲函數)[7]參考資料

[1]Defer statements: https://golang.google.cn/ref/spec#Defer_statements[2]Defer_statements: https://golang.google.cn/ref/spec#Defer_statements[3]go語言的defer語句: https://www.jianshu.com/p/5b0b36f398a2[4]Go defer實現原理剖析: https://studygolang.com/articles/16067[5]go語言 defer 你不知道的秘密!: https://www.cnblogs.com/baizx/p/5024547.html[6]Go語言中defer的一些坑: https://www.jianshu.com/p/79c029c0bd58[7]go defer (go延遲函數): https://www.cnblogs.com/ysherlock/p/8150726.html

相關焦點

  • Go 語言之 defer 的前世今生
    下面我們就來詳細看看這兩個調用具體發生了什麼事情。我們先看創建 defer 的第一種形式 deferproc。這個調用很簡單,僅僅只是將需要被 defer 調用的函數做了一次記錄: 1//go:nosplit 2func deferproc(siz int32, fn *funcval) { 3  ...
  • Go 語言之 defer 的前世今生 - CSDN
    編譯階段為了使延遲語句的功能滿足語言規範,該語句在編譯的 SSA 階段會被翻譯為兩個主體,其中第一個主體是被延遲的函數本身,另一個主體則是函數結束時需要執行所記錄 defer 的代碼塊。16}附著在 Goroutine 上的 _defer 記錄的鍊表現在我們知道,一個在堆上分配的延遲語句被編譯為了 deferproc,用於記錄被延遲的函數調用;在函數的尾聲,會插入 deferreturn 調用,用於執行被延遲的調用。下面我們就來詳細看看這兩個調用具體發生了什麼事情。
  • 深入理解 Go 語言 defer
    閉包簡單來說,Go 語言中的閉包就是在函數內引用函數體之外的數據,這樣就會產生一種結果,雖然數據定義是在函數外,但是在函數內部操作數據也會對數據產生影響。如下面的例子所示,foo() 中的匿名函數對 i 的調用就是閉包引用,i++ 會影響外面定義的 i 的值。而 bar() 中的匿名函數是變量拷貝,i++ 並不會修改外部 i 值。
  • Go語言defer你不知道的事
    ,每次 defer 都會把一個函數壓入棧中,函數返回前再把延遲的函數取出並執行。後面對變量 i 的修改不會影響 fmt.Println() 函數的執行,仍然列印 "0"。注意:對於指針類型參數,規則仍然適用,只不過延遲函數的參數是一個地址值,這種情況下,defer 後面的語句對變量的修改可能會影響延遲函數。
  • Go 經典入門系列 28:Defer
    什麼是 defer? defer 語句的用途是:含有 defer 語句的函數,會在該函數將要返回之前,調用另一個函數。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。largest 函數接收一個 int 類型的切片[3]作為參數,然後列印出該切片中的最大值。largest 函數的第一行的語句為 defer finished()。這表示在 finished() 函數將要返回之前,會調用 finished() 函數。
  • 『GCTT 出品』Go 中 defer 的 5 個坑 - 第一部分
    首發於:https://studygolang.com/articles/12061Go 中 defer 的 5 個坑 - 第一部分通過本節的學習以避免掉入基礎的 defer 陷阱中本文只適合想要進階學習 Golang 的新手閱讀,大牛請繞道。
  • Go 的 defer 的特性還是有必要要了解下的!!!
    Golang 的 defer 是什麼?通俗來講就是延遲調用。defer 會在當前函數返回之前執行 defer 註冊的函數。比如 defer func_x( )  這樣語句會讓你註冊一個函數變量到 defer 的全局鍊表中,在 defer 語句所在的函數退出之前調用。
  • 解密 defer 原理,究竟背著程序猿做了多少事情?
    和特性上分析了 defer 關鍵字,讓我們對此有個形象的概念,然後剖析了函數調用的本質原理 ( 深入剖析 defer 原理篇 —— 函數調用的原理? ),接下來剖析就是真正 defer 這個關鍵字背後的原理了。思考幾個問題:一個函數內多個 defer 語句的時候,會發生什麼?
  • Golang中defer的實現原理
    所以,defer後面的函數通常又叫做延遲函數defer規則1.延遲函數的參數在defer語句出現時就已經確定下來了func a() {    i := 0    defer fmt.Println(i)    i++    return}
  • golang的defer使用相關
    () defer func() { fmt.Println(2) }() defer func() { fmt.Println(3) }()}如果我們不去執行上面的代碼,我們能看出來執行的結果是什麼嗎?
  • Golang之defer進階
    每個測試函數defer的列印如下:test1:defer執行時,對Printf的入參x=0進行計算。return 9後執行Printf,所以結果是in defer: x = 0。test2:與test1不同的是,defer執行是在x=7之後,所以x的值是7,並且傳遞給Printf,所以結果是:in defer: x = 7。
  • 深入剖析 defer 原理篇 —— 函數調用的原理?
    地址空間函數棧幀棧幀的劃定函數調用函數返回舉個例子總結本篇文章是深入剖析 golang 的 defer 的基礎知識準備,如果要完全理解 defer ,避免踩坑,這個章節的基礎知識必不可少我們先複習一個最基礎的知識 —— 函數調用。這個對理解 defer 在函數裡的行為必不可少。那麼,當你看到一個函數調用的語句你能回憶起多少知識點呢?
  • Golang 之輕鬆化解 defer 的溫柔陷阱
    作者 | 饒全成責編 | 胡巍巍defer是Go語言提供的一種用於註冊延遲調用的機制:讓函數或語句可以在當前函數執行完畢後(包括通過return正常結束或者panic導致的異常結束)執行。為了更好的閱讀體驗,按慣例我手動貼上文章目錄:什麼是defer?defer是Go語言提供的一種用於註冊延遲調用的機制:讓函數或語句可以在當前函數執行完畢後(包括通過return正常結束或者panic導致的異常結束)執行。
  • golang中defer的執行過程詳解
    defer是go的關鍵字。本文通過go彙編信息,深入分析了defer的調用棧原理在同一個goroutine中:多個defer的調用棧原理是什麼?defer函數是如何調用的?(SB)發現aaa()函數的參數及調用函數deferproc(SB): 0x0021 00033 (main.go:10) MOVL $24, (SP) 0x0028 00040 (main.go:10) PCDATA $2, $1 0x0028 00040 (main.go:10) LEAQ "".aaa
  • go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包
    本篇文章是 Go 語言學習筆記之函數式編程系列文章的第二篇,上一篇介紹了函數基礎,這一篇文章重點介紹函數的重要應用之一: 閉包空談誤國,實幹興邦,以具體代碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例代碼開始本文的學習吧
  • 詳解defer實現機制(附上三道面試題,我不信你們都能做對)
    函數在返回時,首先函數返回時會自動創建一個返回變量假設為ret(如果是命名返回值的函數則不會創建),函數返回時要將變量i賦值給ret,即有ret = i。然後檢查函數中是否有defer存在,若有則執行defer中部分。現在你們應該知道上面是什麼原因了吧~。
  • 超詳細go入門筆記
    package mainimport "fmt"func main() { fmt.Println("hello word")}第2章 Go基本語法2.1變量2.1.1. go語言中變量分為局部變量和全局變量•局部變量,是定義在打括號{}內部的變量,打括號內部也是局部變量的作用域•全局變量,是定義在函數和打括號外部{}的變量
  • Golang-Defer
    Go官方文檔中對defer的執行時機做了闡述,分別是。包裹defer的函數返回時包裹defer的函數執行到末尾時所在的goroutine發生panic時defer執行順序當一個方法中有多個defer時, defer會將要延遲執行的方法「壓棧」,當defer被觸發時,將所有「壓棧」的方法「出棧」並執行。
  • Go 經典入門系列 32:panic 和 recover
    發生 panic 時的 defer 我們重新總結一下 panic 做了什麼。當函數發生 panic 時,它會終止運行,在執行完所有的延遲函數後,程序控制返回到該函數的調用方。這樣的過程會一直持續下去,直到當前協程的所有函數都返回退出,然後程序會列印出 panic 信息,接著列印出堆棧跟蹤,最後程序終止。
  • Golang中的Defer必掌握的7知識點
    一個函數中,寫在前面的defer會比寫在後面的defer調用的晚。,就會在函數初始化時候為之賦值為0,而且在函數體作用域可見。() { fmt.Println(returnButDefer())}該returnButDefer()本應的返回值是1,但是在return之後,又被defer的匿名func函數執行,所以t=t*10被執行,最後returnButDefer()返回給上層main()的結果為10$ go run test.go10