1. 程式人生 > >go 學習筆記之解讀什麼是defer延遲函式

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

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

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

出入成雙有始有終

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

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

最常見的拷貝檔案操作大致流程如下:

  1. 開啟原始檔
srcFile, err := os.Open("fib.txt")
if err != nil {
    t.Error(err)
    return
}   
  1. 建立目標檔案
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
    t.Error(err)
    return
}
  1. 拷貝原始檔到目標檔案
io.Copy(dstFile, srcFile)
  1. 關閉目標檔案
dstFile.Close()
srcFile.Close()
  1. 關閉原始檔
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

如果沒有 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
  • go語言的defer語句
  • Go defer實現原理剖析
  • go語言 defer 你不知道的祕密!
  • Go語言中defer的一些坑
  • go defer (go延遲函式)

如果本文對你有所幫助,不用讚賞,點贊鼓勵一下就是最大的認可,順便也可以關注下微信公眾號「 雪之夢技術驛站 」喲!

相關推薦

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

Go 語言中有個 defer 關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患". 延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了. 出入成雙有始有終

三、PYTHON 學習筆記 join 和 split 函式用法

python join 和 split方法的使用,join用來連線字串,split恰好相反,拆分字串的。 1.join用法示例  >>>li = ['my','name','is

吳恩達機器學習 學習筆記 二 :代價函式和梯度下降演算法

二、 2-1 Model Representation 我們學習的第一個演算法是線性迴歸,接下來會講什麼樣的模型更重要,監督學習的過程是什麼樣子。 首先舉一個需要做預測的例子:住房價格上漲,預測房價,我們擁有某一城市的住房價格資料。基於這些資料,繪製圖形。 在已有房價資

Go學習筆記高階資料型別

高階資料型別,僅僅是做個概念認識,等到其他相關知識的學習時,再著重分析。 1 function 將 function 作為資料型別的語言有很多,函數語言程式設計的核心理念。 function 是“第一等公民”,function 與其他資料型別一樣,處於平等地位,可以賦值給

go 學習筆記初識 go 語言

Go 是一種開源程式語言,可以輕鬆構建簡單,可靠,高效的軟體. 摘錄自 github: https://github.com/golang/go,其中官網(國外): https://golang.org 和官網(國內): https://golang.google.cn/ Go 是 Google 公司

go 學習筆記環境搭建

千里之行始於足下,開始 Go 語言學習之旅前,首先要搭建好本地開發環境,然後就可以放心大膽瞎折騰了. Go 的環境安裝和其他語言安

go 學習筆記工作空間

搭建好 Go 的基本環境後,現在可以正式開始 Go 語言的學習之旅,初學時建議在預設的 GOPATH 工作空間規範編寫程式碼,基本目錄結構大概是這個樣子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.

go 學習筆記走進Goland編輯器

工欲善其事必先利其器,命令列工具雖然能夠在一定程度上滿足基本操作的需求,但實際工作中總不能一直使用命令列工具進行編碼操作吧? 學習 Go 語言同樣如此,為此需要尋找一個強大的 IDE 整合環境幫助我們快速開發,據我所知,市面上比較流行的可能有三個選擇: LiteIDE X : LiteIDE 是一款簡單,開

go 學習筆記有意思的變數和不安分的常量

首先希望學習 Go 語言的愛好者至少擁有其他語言的程式設計經驗,如果是完全零基礎的小白使用者,本教程可能並不適合閱讀或嘗試閱讀看看,系列筆記的目標是站在其他語言的角度學習新的語言,理解 Go 語言,進而寫出真正的 Go 程式. 程式語言中一般都有變數和常量的概念,對於學習新語言也是一樣,變數指的是不同程式語言

go 學習筆記值得特別關注的基礎語法有哪些

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變數和不安分的常量. 相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變數和常量與其他主要的程式語言的異同,為了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變數和常量

go 學習筆記陣列還是切片都沒什麼不一樣

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的程式語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的程式語言切換到 Go 語言的程式設計習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美! 在學習 Go 的內建容器前,同樣的,

go 學習筆記go是不是面嚮物件語言是否支援面對物件程式設計?

面向物件程式設計風格深受廣大開發者喜歡,尤其是以 C++, Java 為典型代表的程式語言大行其道,十分流行! 有意思的是這兩中語言幾乎毫無意外都來源於 C 語言,卻不同於 C 的面向過程程式設計,這種面向物件的程式設計風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成為設計的標準,從而讓我們

go 學習筆記詳細說一說封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計? 雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現. 但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可

go 學習筆記是否支援以及如何實現繼承

熟悉面向物件的小夥伴們可能會知道封裝,繼承和多型是最主要的特性,為什麼前輩們會如此看重這三種特性,真的那麼重要嗎? 什麼是封裝 什麼是封裝,封裝有什麼好處以及怎麼實現封裝? 相信大多數小夥伴們都有自己的理解,簡而言之,言而簡之,封裝是遮蔽內部實現細節,僅僅對外暴露出有價值介面. 正如平時工作中使用的電

go 學習筆記萬萬沒想到寵物店竟然催生出面向介面程式設計?

到底是要貓還是要狗 在上篇文章中,我們編撰了一則簡短的小故事用於講解了什麼是面向物件的繼承特性以及 Go 語言是如何實現這種繼承語

go 學習筆記無心插柳柳成蔭的介面和無為而治的空介面

如果你還了解程式設計概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點選 go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計? ,否則的話,請自動忽略上文,繼續探索 Go 語言的介面有什麼不同之處. 如無法自動跳轉到公眾號「雪之夢技術驛站」文章,可以點選我的頭像,動動你的小手翻翻歷史文

go 學習筆記僅僅需要一個示例就能講清楚什麼閉包

本篇文章是 Go 語言學習筆記之函數語言程式設計系列文章的第二篇,上一篇介紹了函式基礎,這一篇文章重點介紹函式的重要應用之一: 閉包 空談誤國,實幹興邦,以具體程式碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例程式碼開始本文的學習吧! 斐波那契數列是形如 1 1 2 3 5

go 學習筆記10 分鐘簡要理解 go 語言閉包技術

閉包是主流程式語言中的一種通用技術,常常和函數語言程式設計進行強強聯合,本文主要是介紹 Go 語言中什麼是閉包以及怎麼理解閉包. 如果讀者對於 Go 語言的閉包還不是特別清楚的話,可以參考上一篇文章 go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包. 或者也可以直接無視,因為接下來會回顧一下前情概要,現在你

c++學習筆記成員函式

學了c++才知道什麼是面向物件什麼是面向過程。幼稚的我曾經還覺得c++和C語言差不多,接觸之後才知道c++是多麼的難,光類和物件這個知識點就看了一天。什麼建構函式解構函式,物件陣列物件成員弄得我頭大,現在才知道為什麼老師說c++是最難的語言,學c++就是在地獄裡磨鍊,從地獄出來就是天堂,會有會當凌絕

go學習筆記-函式

函式 定義 格式 func function_name( [parameter list] ) [return_types] { 函式體 } 解析 func:函式由 func 開始宣告 function_name:函式名稱,函式名和引數列表一起構成了函式簽名。 parameter