go 學習筆記之咬文嚼字帶你弄清楚 defer 延遲函數

溫故知新不忘延遲基礎

go-error-defer-learn-from-old.jpg

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.

延遲函數的運行時機通常有三種狀況:html

  • 周圍函數遇到返回時
func funcWithMultipleDeferAndReturn() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
    return
    fmt.Println(4)
}
運行結果: 3 2 1 .

「雪之夢技術驛站」: defer fmt.Println(1)defer fmt.Println(2) 兩個語句因爲前面存在 defer 關鍵字,所以均被延遲到正常語句 return 前.當多個 defer 語句均被延遲時,倒序執行延遲語句,這種特色很是相似於數據結構的(先入後出).因此依次輸出 fmt.Println(3) ,defer fmt.Println(2) ,defer fmt.Println(1) .git

  • 周圍函數函數體結尾處
func funcWithMultipleDeferAndEnd() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
}
運行結果: 3 2 1 .

「雪之夢技術驛站」: 比 funcWithMultipleDeferAndReturn 示例簡單一些,雖然包圍函數 funcWithMultipleDeferAndEnd 並無顯示聲明 return 語句,可是當函數運行結束前依然不會忘記執行延遲語句.因此 fmt.Println(3) 執行完後,程序並無當即結束而是緊接着執行延遲語句 defer fmt.Println(2)defer fmt.Println(1).github

  • 當前協程惶恐不安中
func funcWithMultipleDeferAndPanic() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
    panic("panic")
    fmt.Println(4)
}
運行結果: 3 2 1 .

「雪之夢技術驛站」: 和 funcWithMultipleDeferAndReturn 示例有點相似,只不過由原來的 return 語句換成了 panic("panic"). 咱們知道延遲語句 defer fmt.Println(1)defer fmt.Println(2) 確定會被延遲執行,因此並不會先輸出 1,2 而是先執行了 fmt.Println(3) ,下一步就遇到了 panic("panic") ,此時顧不上惶恐不安,先讓已存在的 defer 語句先執行再說!
同時,defer 是倒序執行的,於是先輸出 defer fmt.Println(2) 再輸出 defer fmt.Println(1) ,最後完成使命,光榮掛掉,至於 fmt.Println(4) 就沒法執行了!golang

關於這一句話的詳細解讀,請參考 go 學習筆記之解讀什麼是defer延遲函數,示例源碼見 snowdreams1006/learn-go/tree/master/errorweb

若是你真的試圖去理解 defer執行時機,最好看一下彙編代碼的具體實現,推薦一下大佬的 defer關鍵字express

關於 defer 關鍵字相關解釋,摘錄以下:c#

當函數包含 defer 語句,則彙編代碼:

c
call runtime.deferreturn,
add xx SP
returnsegmentfault

goroutine 的控制結構中,有一張表記錄 defer ,調用 runtime.deferproc 時會將須要 defer 的表達式記錄在表中,而在調用 runtime.deferreturn 的時候,則會依次從 defer 表中出棧並執行。數組

可是,從語義上理解會更加簡單,問一下本身爲何須要 defer 關鍵字,到底解決了什麼問題?緩存

go-error-defer-question.png

一旦理解了 defer 關鍵字的實現意圖,那麼天然而然就能大概猜出有關執行順序,因此何須深究實現細節呢?

簡而言之,defer 關鍵字是確保程序必定會執行的代碼邏輯,無論程序是正常 return 仍是意外 panic ,包圍函數一旦存在 defer 關鍵字就要保證延遲函數必定執行!

當存在多個 defer 關鍵字時,意味着有多個緊急任務須要處理,時間緊迫,固然是事故發生點最近的優先執行,離returnpanic 越遠的越晚執行.

因此以防萬一和就近原則是理解 defer 執行時機的最佳途徑: 萬一哪天發生火災,第一反應天然是就近救人啊!

go-error-defer-fire-and-save.jpg

支持什麼又不支持哪些

The expression must be a function or method call; it cannot be parenthesized. Calls of built-in functions are restricted as for expression statements.
  • 支持函數調用
func funcCallWithDefer() {
    fmt.Println("funcInvokeWithDefer function is called")
}

func TestFuncCallWithDefer(t *testing.T) {
    // 「雪之夢技術驛站」: defer 語句能夠是函數調用.
    fmt.Println(" 「雪之夢技術驛站」: defer 語句能夠是函數調用.")

    defer funcCallWithDefer()

    fmt.Println("TestFuncInvokeWithDefer function call has ended")
}
  • 支持方法調用
type Lang struct {
    name    string
    website string
}

func (l *Lang) ToString() {
    fmt.Printf("Lang:[name = %s,website = %s] \n", l.name, l.website)
}

func TestMethodCallWithDefer(t *testing.T) {
    // 「雪之夢技術驛站」: defer 語句也能夠是方法調用.
    fmt.Println(" 「雪之夢技術驛站」: defer 語句也能夠是方法調用.")

    var l = new(Lang)
    l.name = "Go"
    l.website = "https://snowdreams1006.github.io/go/"

    defer l.ToString()

    fmt.Println("TestMethodCallWithDefer method call has ended")
}
  • 不能夠被括號包裹

go-error-defer-parenthesized-fail.png

  • 內建函數和表達式同樣受限
函數名 說明 說明
close 關閉channel 僅用於channel通信
delete 從map中刪除實例 map操做
len 返回字符串,slice和數組的長度 可用於不一樣的類型
cap 返回容量 可用於不一樣的類型
new 內存分配 用於各類類型
make 內存分配 僅用於chan/slice/map
copy 複製slice slice操做
append 追加slice slice操做
panic 報告運行時問題 異常處理機制
recover 處理運行時問題 異常處理機制
print 內建打印函數 主要用於不引入fmt的時候的調試,實際使用時建議使用標準庫fmt
println 內建打印函數 主要用於不引入fmt的時候的調試,實際使用時建議使用標準庫fmt
complex 構造複數類型 複數操做
real 抽出複數的實部 複數操做
imag 抽出複數的虛部 複數操做
func TestBuiltinFuncCallWithDefer(t *testing.T) {
    // 「雪之夢技術驛站」: defer 語句不能夠被括號包裹.
    fmt.Println(" 「雪之夢技術驛站」: defer 語句不能夠被括號包裹.")

    arr := new([10]int)
    arr[4] = 5
    arr[7] = 8

    // defer discards result of len(arr)
    defer len(arr)
    defer println("Calls of built-in functions are restricted as for expression statements.")

    fmt.Println("TestBuiltinFuncCallWithDefer function call has ended")
}

咬文嚼字深刻理解延遲

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the "defer" statement is executed.

打蛇打七寸

go-error-defer-emphasis.jpeg

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.

每次延遲語句執行時,函數值和調用參數會像以往同樣被評估和保存,可是實際函數並不會被調用.

func trace(funcName string) func(){
    start := time.Now()
    fmt.Printf("function %s enter at %s \n",funcName,start)

    return func(){
        fmt.Printf("function %s exit at %s(elapsed %s)",funcName,time.Now(),time.Since(start))
    }
}

func foo(){
    fmt.Printf("foo begin at %s \n",time.Now())

    defer trace("foo")()
    time.Sleep(5*time.Second)

    fmt.Printf("foo end at %s \n",time.Now())
}

func TestFoo(t *testing.T) {
    foo()
}

trace 函數實現了函數計時功能,而 foo 函數則是包圍函數用於演示 defer 關鍵字的邏輯,TestFoo 是測試函數,輸出測試結果.

測試結果以下:

=== RUN TestFoo
foo begin at 2019-11-18 23:12:38.519097 +0800 CST m=+0.000735902
function foo enter at 2019-11-18 23:12:38.519287 +0800 CST m=+0.000926011
foo end at 2019-11-18 23:12:43.524445 +0800 CST m=+5.005934027
function foo exit at 2019-11-18 23:12:43.524549 +0800 CST m=+5.006038281(elapsed > 5.005112612s)--- PASS: TestFoo (5.01s)
PASS

Process finished with exit code 0

若是此時試圖去解釋上述運行結果,很遺憾鎩羽而歸!

go-error-defer-foo-explain-fail.jpeg

記得官方文檔中關於 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.

可是若是按照這句話來解釋這次示例的運行結果,顯然是解釋不通的!

func foo(){
    fmt.Printf("foo begin at %s \n",time.Now())

    defer trace("foo")()
    time.Sleep(5*time.Second)

    fmt.Printf("foo end at %s \n",time.Now())
}

func TestFoo(t *testing.T) {
    foo()
}

若是 defer trace("foo")() 延遲函數真的被延遲到函數體結束以前,那麼上述 foo() 函數應該等價於這種形式:

func fooWithoutDefer(){
    fmt.Printf("foo begin at %s \n",time.Now())

    time.Sleep(5*time.Second)

    fmt.Printf("foo end at %s \n",time.Now())

    trace("foo")()
}

func TestFooWithoutDefer(t *testing.T) {
    fooWithoutDefer()
}

可是對於 fooWithoutDefer 函數的執行結果直接實力打臉:

=== RUN TestFooWithoutDefer
foo begin at 2019-11-19 11:44:20.066554 +0800 CST m=+0.001290523
foo end at 2019-11-19 11:44:25.068724 +0800 CST m=+5.003312582
function foo enter at 2019-11-19 11:44:25.068796 +0800 CST m=+5.003384341
function foo exit at 2019-11-19 11:44:25.068847 +0800 CST m=+5.003435185(elapsed 51.196µs)--- PASS: TestFooWithoutDefer (5.00s)
PASS

Process finished with exit code 0

因而可知,延遲函數其實並不簡單,想要弄清楚 defer 關鍵字還要繼續讀下去纔有可能!

go-error-defer-foo-result-diff.png

這一點也是我最大的疑惑,潛意識告訴我: 只要沒法真正理解 Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. 這句話的含義,那麼永遠不可能完全弄清 defer 關鍵字!

經過直接調換 defer 語句的出現位置並無解釋測試結果,反而告訴咱們 defer 語句可不是簡簡單單的延遲執行.

任何函數都會或多或少依賴相應的執行環境,defer 延遲函數也不例外,在本示例中 defer trace("foo")() 延遲函數的 trace("foo") 函數的返回值是函數,而後 trace("foo")() 至關於當即執行返回函數,於是問題可能出如今 trace("foo") 函數中,那麼不妨繼續看看吧!

func foo(){
    fmt.Printf("foo begin at %s \n",time.Now())

    defer trace("foo")()
    time.Sleep(5*time.Second)

    fmt.Printf("foo end at %s \n",time.Now())
}

func trace(funcName string) func(){
    start := time.Now()
    fmt.Printf("function %s enter at %s \n",funcName,start)

    return func(){
        fmt.Printf("function %s exit at %s(elapsed %s)",funcName,time.Now(),time.Since(start))
    }
}

1. foo begin at 2019-11-19 14:06:42.385982 +0800 CST m=+0.000943615 暗示着已經開始進入 foo() 函數內部,接下來的 function foo enter at 2019-11-19 14:06:42.38623 +0800 CST m=+0.001191025 意味着函數並無執行 time.Sleep(5*time.Second) 而是直接進入了 defer trace("foo")() 語句內部,可見函數依舊是順序執行,可是 trace(funcName string) func() 函數內部會返回函數,此時函數返回值並無執行,由於此時並不存在打印輸出的日誌.

因此 trace(funcName string) func() 函數應該是已經執行了,接下來返回上一層回到主函數 foo() 就遇到了 time.Sleep(5*time.Second) 休息 5s 語句,因此在執行 fmt.Printf("foo end at %s \n",time.Now()) 語句時輸出的時間和最近的上一句差了大概 5s .

foo end at 2019-11-19 14:06:47.391581 +0800 CST m=+5.006394415 輸出後也就意味着 foo() 函數運行到包圍函數的結束處,此時按照延遲語句的第一句,咱們知道是時候執行真正的延遲邏輯了.

因此下一句就是 trace("foo")() 的函數返回值的調用,輸出了 function foo exit at 2019-11-19 14:06:47.391706 +0800 CST m=+5.006518615(elapsed 5.005327927s)--- PASS: TestFoo (5.01s)

至此,延遲函數執行完畢,單元測試函數也輸出了 PASS .

=== RUN   TestFoo
foo begin at 2019-11-19 14:06:42.385982 +0800 CST m=+0.000943615 
function foo enter at 2019-11-19 14:06:42.38623 +0800 CST m=+0.001191025 
foo end at 2019-11-19 14:06:47.391581 +0800 CST m=+5.006394415 
function foo exit at 2019-11-19 14:06:47.391706 +0800 CST m=+5.006518615(elapsed 5.005327927s)--- PASS: TestFoo (5.01s)
PASS

經過上述分析,能夠這麼理解,延遲函數也是須要執行環境的,而執行環境就是依賴於定義 defer 語句時的相關環境,這也就是延遲函數的準備階段或者說入棧.

當遇到包圍函數體返回時或到達包圍函數體結尾處或發生錯誤時,包圍函數就會調用已存在的延遲函數,這部分就是延遲函數的執行階段或者說出棧.

  • 不管是否存在延遲函數,均順序執行函數邏輯
  • 準備階段的入棧操做會正常運行但不會調用函數
  • 執行階段的出棧操做在合適時機時會調用函數

一樣地,仍然以消防隊員做爲 Go 的調度器,平民百姓做爲無 defer 保護的對比參考,而有 defer 保護的特殊人羣做爲延遲函數.

有一天,普通百姓和特殊人士都在商場逛街,突發火災,附近消防員迅速趕忙救人,任務只要一個:那就是按照就近原則快速救出所有特殊人羣,由於這些特殊人羣都是有頭有臉的人物,每一個人都有本身的脾氣個性.

明星 A : 我進商場前拿着限量版的 LV 包包,這個我也要拿出去!
富二代 B : 我進商場前答應小女朋友要給他買個禮物,這個是寄存櫃地址,別忘了把禮物也帶回來!
暴發戶 C : 我在商場有個保險櫃,存放了大量金條,必定要給我帶出去!

消防員很無奈,內心咒罵了一句: 這都生死攸關了,還管什麼身外之物啊!

但是,埋怨歸埋怨,對於這些特殊人羣的照顧,那是一丁點也不敢怠慢,只能照辦,終於所有救出了!

A 表示聲明 defer 語句時已經傳遞了參數,等到執行 defer 時調用的就是剛纔的參數值,而 Go 語言中參數的傳遞只能是 值傳遞,因此雖然看起來仍是那個包,其實已經變了,這裏並非特別準確!
B 表示聲明 defer 語句時傳遞的參數不是具體值而是引用,當執行 defer 邏輯時會按圖索驥,所以雖然給的是一張寄存櫃的密碼紙,最後拿出來的倒是存在櫃子裏的禮物.
C 表示聲明 defer 時什麼都沒有傳遞,沒有任何入參可是執行 defer 語句中遇到了訪問包圍函數的需求,這時候延遲函數會擴大搜索範圍向上尋找直到找到商場的金庫爲止.
  • 零依賴而無顧慮
func deferWithoutParams() {
    // 2 1
    defer fmt.Println(1)
    fmt.Println(2)
}
「雪之夢技術驛站」: 入棧時沒有任何依賴,出棧時也不會有任何顧慮,很是簡單直觀輸出了 2 1 .
  • 隨身攜帶的牽掛
func deferWithValueParams() {
    x := 10
    defer func(n int) {
        // 10
        fmt.Println(n)
    }(x)
    x++
}
「雪之夢技術驛站」: 入棧時存在值參數 func(n int)(10),出棧時須要輸出參數的值,而 fmt.Println(n) 涉及到的 n 恰好保存在入棧環境中,因此等到 deferWithValueParams 運行到函數結束後輸出的結果就是已緩存的副本 10 .

若是此時匿名函數調用的不是 n 而是 x,而變量 x 並不存在於入棧環境中,此時就會繼續擴大範圍搜到 deferWithValueParams 函數是否存在變量 x 的聲明,本示例中找到的 x=11.

func deferWithOuterParams() {
    x := 10
    defer func(n int) {
        // 11
        fmt.Println(x)
    }(x)
    x++
}
  • 心有牽掛放不下
func deferWithReferParams() {
    x := 10
    defer func(n *int) {
        // 11
        fmt.Println(*n)
    }(&x)
    x++
}
「雪之夢技術驛站」: 入棧時保存的再也不是值而是地址,所以出棧時會按圖索驥,找到該地址對應的值,也就是 11 .

相信以上案例應該幫助讀者理解 defer 語句的一些注意事項了吧?

延遲函數準備階段的入棧會收集函數運行所需的環境依賴,好比說入參的值,收集結束後即便外界再改變該值也不會影響延遲函數,由於延遲函數用的是緩存副本啊!

出棧會倒序

Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.

相反的,延遲函數會在包圍函數返回以前按照被延遲順序逆序調用.

func TestFuncWithMultipleDefer(t *testing.T) {
    // 「雪之夢技術驛站」: 猜想 defer 底層實現數據結構多是棧,先進後出.
    t.Log(" 「雪之夢技術驛站」: 猜想 defer 底層實現數據結構多是棧,先進後出.")

    // 3 2 1
    defer t.Log(1)
    defer t.Log(2)
    t.Log(3)
}
「雪之夢技術驛站」: 運行階段的出棧操做會倒序執行多個 defer 延遲函數,因此輸出了 3 2 1 .

及時雨插入

That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.

當包圍函數經過明確的 return 返回語句返回時,defer 延遲函數會在 result parameters 結果參數被賦值以後且在函數 return 返回以前執行.

按照這句話能夠將下面這種代碼進行拆解:

defer yyy
return xxx

其中 return xxx 至關於拆開了兩步而且最終返回前及時插入了 defer 語句的執行邏輯,以下:

1. result parameters = xxx
2. 調用 defer 函數
3. return

一樣地,咱們舉例說明:

func deferWithExplicitReturn() (result int) {
    defer func() {
        // 2. before : result = 10
        fmt.Printf("before : result = %v\n", result)

        result++

        // 3. after : result = 11
        fmt.Printf("after : result = %v\n", result)
    }()

    result = 10

    // 1. return : result = 10
    fmt.Printf("return : result = %v\n", result)

    return result
}

關於 defer 延遲函數的執行順序和輸出結果已經再也不是難點了,如今主要關注下 deferWithExplicitReturn() 函數運行結束後的返回值究竟是 10 仍是 11 .

func TestDeferWithExplicitReturn(t *testing.T) {
    // TestDeferWithExplicitReturn result = 11
    fmt.Printf("TestDeferWithExplicitReturn result = %d\n",deferWithExplicitReturn())
}
「雪之夢技術驛站」: 測試結果輸出了 11,很顯然這裏是由於延遲函數內部執行了 result++ 操做最終影響了外部函數的返回值.

若是對上述示例進行改造,下面的代碼就清晰看出了爲何會影響返回值了.

func deferWithExplicitReturnByExplain() (result int) {
    result = 10

    // 1. return : result = 10
    fmt.Printf("return : result = %v\n", result)
    
    func() {
        // 2. before : result = 10
        fmt.Printf("before : result = %v\n", result)

        result++

        // 3. after : result = 11
        fmt.Printf("after : result = %v\n", result)
    }()

    return
}
「雪之夢技術驛站」: 延遲函數會在 return 返回前有機會對返回值進行更改,這裏演示了及時雨插入的邏輯,輸出結果不變仍是 11.

go-error-defer-deferWithExplicitReturnByExplain-result.png

下面提供一些例題,請自行思考

func surroundingFuncEvaluatedNotInvoked(init int) int {
    fmt.Printf("1.init=%d\n",init)

    defer func() {
        fmt.Printf("2.init=%d\n",init)

        init ++

        fmt.Printf("3.init=%d\n",init)
    }()

    fmt.Printf("4.init=%d\n",init)

    return init
}

func noDeferFuncOrderWhenReturn() (result int) {
    func() {
        // 1. before : result = 0
        fmt.Printf("before : result = %v\n", result)

        result++

        // 2. after : result = 1
        fmt.Printf("after : result = %v\n", result)
    }()

    // 3. return : result = 1
    fmt.Printf("return : result = %v\n", result)

    return 0
}


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

func deferFuncWithNamedReturnValue() (retVal int) {
    defer func() {
        retVal++
    }()
    return 0
}
「雪之夢技術驛站」: 若是一眼看不出答案,不妨複製到編輯器直接運行,而後在思考爲何.

調用時報錯

If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the "defer" statement is executed.

若是延遲函數值爲 nil,則函數調用時發生錯誤異常 panic 而不是 defer 語句執行時報錯.

func deferWithNil() func() {
    return nil
}

func TestDeferWithNil(t *testing.T) {
    fmt.Println("begin exec deferWithNil()()")

    defer deferWithNil()()

    fmt.Println("end exec deferWithNil()()")
}

go-error-defer-deferWithNil-result.png

公佈答案以及總結全文

在上篇文章中留下了兩個小問題,相信看到這篇文章的人都能獨立完成並自行解釋了吧?

下面給出問題以及答案!

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

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

func TestDeferFuncWhenReturn(t *testing.T) {
    // 0
    t.Log(deferFuncWithAnonymousReturnValue())
    // 1
    t.Log(deferFuncWithNamedReturnValue())
}
「雪之夢技術驛站」: deferFuncWithAnonymousReturnValue() 函數無明確的返回值參數,而 deferFuncWithNamedReturnValue() 函數已經聲明瞭 (retVal int) 返回值,由於延遲函數並不會影響未命名的函數.

經過本文,咱們知道了延遲函數的執行時機以及一些細節,關鍵是理解 Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. 這句話,絕對是重中之重!

簡而言之,延遲函數在聲明時會收集相關參數賦值拷貝一份入棧,時機合適時再從入棧環境中尋找相關環境參數,若是找不到就擴大範圍尋找外層函數是否包含所需變量,執行過程也就是延遲函數的出棧.

有一個消防員專門負責保衛商場的安全,天天商場進進出出不少人流,總有一些重要人物也會來到商場購物,忽然有一天,發生了火災,正在你們惶恐不安中...

這個消防員到底幹了什麼才能保證重要人物安全的同時也能讓他們不遭受財產損失?

go-error-defer-panic.png

請補充你的答案,感謝你的閱讀與關注,下一節再見~

閱讀延伸以及參考文檔

若是本文對你有所幫助,不用讚揚,也沒必要轉發,直接點贊留言告訴鼓勵一下就能夠啦!

雪之夢技術驛站.png

相關文章
相關標籤/搜索