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

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

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

go-error-about-defer.jpg

出入成雙有始有終

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

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

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

  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 延遲函數?

go-error-about-lovely.png

延遲函數應用場景

基本上成雙成對的操作都可以使用延遲函數,尤其是申請的資源前後存在依賴關係時更應該使用 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 延遲函數,通過解讀官方文檔並配套相關代碼認識了延遲函數,但是延遲函數中存在一些可能令人比較迷惑的地方.

go-error-about-question.png

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

func deferFuncWithAnonymousReturnValue() int {
    var retVal int
    defer func() {
        retVal++
    }()
    return 0
}

func deferFuncWithNamedReturnValue() (retVal int) {
    defer func() {
        retVal++
    }()
    return 0
}

延伸閱讀參考文檔

如果本文對你有所幫助,不用讚賞,也不必轉發,直接點贊留言告訴鼓勵一下就可以啦!

雪之夢技術驛站.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章