go 學習筆記之咬文嚼字帶你弄清楚 defer 延遲函數
- 2019 年 11 月 20 日
- 筆記
溫故知新不忘延遲基礎
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 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)
.
- 周圍函數函數體結尾處
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)
.
- 當前協程驚慌失措中
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)
就無法執行了!
關於這一句話的詳細解讀,請參考 go 學習筆記之解讀什麼是defer延遲函數,示例源碼見 snowdreams1006/learn-go/tree/master/error
如果你真的試圖去理解 defer
的執行時機,最好看一下彙編代碼的具體實現,推薦一下大佬的 defer關鍵字
關於 defer
關鍵字相關解釋,摘錄如下:
> 當函數包含 defer
語句,則彙編代碼:
> c > call runtime.deferreturn, > add xx SP > return
> goroutine
的控制結構中,有一張表記錄 defer
,調用 runtime.deferproc
時會將需要 defer
的表達式記錄在表中,而在調用 runtime.deferreturn
的時候,則會依次從 defer
表中出棧並執行。
但是,從語義上理解會更加簡單,問一下自己為什麼需要 defer
關鍵字,到底解決了什麼問題?
一旦理解了 defer
關鍵字的實現意圖,那麼自然而然就能大概猜出有關執行順序,所以何必深究實現細節呢?
簡而言之,defer
關鍵字是確保程序一定會執行的代碼邏輯,不管程序是正常 return
還是意外 panic
,包圍函數一旦存在 defer
關鍵字就要保證延遲函數一定執行!
當存在多個 defer
關鍵字時,意味着有多個緊急任務需要處理,時間緊迫,當然是事故發生點最近的優先執行,離return
或 panic
越遠的越晚執行.
所以以防萬一和就近原則是理解 defer
執行時機的最佳途徑: 萬一哪天發生火災,第一反應自然是就近救人啊!
支持什麼又不支持哪些
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") }
- 不可以被括號包裹
- 內建函數和表達式一樣受限
函數名 |
說明 |
說明 |
---|---|---|
close |
關閉channel |
僅用於channel通訊 |
delete |
從map中刪除實例 |
map操作 |
len |
返回字符串,slice和數組的長度 |
可用於不同的類型 |
cap |
返回容量 |
可用於不同的類型 |
new |
內存分配 |
用於各種類型 |
make |
內存分配 |
僅用於chan/slice/map |
copy |
複製slice |
slice操作 |
append |
追加slice |
slice操作 |
panic |
報告運行時問題 |
異常處理機制 |
recover |
處理運行時問題 |
異常處理機制 |
|
內建打印函數 |
主要用於不引入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.
打蛇打七寸
> 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
如果此時試圖去解釋上述運行結果,很遺憾鎩羽而歸!
記得官方文檔中關於 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
關鍵字還要繼續讀下去才有可能!
這一點也是我最大的疑惑,潛意識告訴我: 只要無法真正理解 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 = %vn", result) result++ // 3. after : result = 11 fmt.Printf("after : result = %vn", result) }() result = 10 // 1. return : result = 10 fmt.Printf("return : result = %vn", result) return result }
關於 defer
延遲函數的執行順序和輸出結果已經不再是難點了,現在主要關注下 deferWithExplicitReturn()
函數運行結束後的返回值到底是 10
還是 11
.
func TestDeferWithExplicitReturn(t *testing.T) { // TestDeferWithExplicitReturn result = 11 fmt.Printf("TestDeferWithExplicitReturn result = %dn",deferWithExplicitReturn()) }
> 「雪之夢技術驛站」: 測試結果輸出了 11
,很顯然這裡是因為延遲函數內部執行了 result++
操作最終影響了外部函數的返回值.
如果對上述示例進行改造,下面的代碼就清晰看出了為什麼會影響返回值了.
func deferWithExplicitReturnByExplain() (result int) { result = 10 // 1. return : result = 10 fmt.Printf("return : result = %vn", result) func() { // 2. before : result = 10 fmt.Printf("before : result = %vn", result) result++ // 3. after : result = 11 fmt.Printf("after : result = %vn", result) }() return }
> 「雪之夢技術驛站」: 延遲函數會在 return
返回前有機會對返回值進行更改,這裡演示了及時雨插入的邏輯,輸出結果不變還是 11
.
下面提供一些例題,請自行思考
func surroundingFuncEvaluatedNotInvoked(init int) int { fmt.Printf("1.init=%dn",init) defer func() { fmt.Printf("2.init=%dn",init) init ++ fmt.Printf("3.init=%dn",init) }() fmt.Printf("4.init=%dn",init) return init } func noDeferFuncOrderWhenReturn() (result int) { func() { // 1. before : result = 0 fmt.Printf("before : result = %vn", result) result++ // 2. after : result = 1 fmt.Printf("after : result = %vn", result) }() // 3. return : result = 1 fmt.Printf("return : result = %vn", 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()()") }
公布答案以及總結全文
在上篇文章中留下了兩個小問題,相信看到這篇文章的人都能獨立完成並自行解釋了吧?
下面給出問題以及答案!
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.
這句話,絕對是重中之重!
簡而言之,延遲函數在聲明時會收集相關參數賦值拷貝一份入棧,時機合適時再從入棧環境中尋找相關環境參數,如果找不到就擴大範圍尋找外層函數是否包含所需變量,執行過程也就是延遲函數的出棧.
有一個消防員專門負責保衛商場的安全,每天商場進進出出很多人流,總有一些重要人物也會來到商場購物,突然有一天,發生了火災,正在大家驚慌失措中…
這個消防員到底幹了什麼才能保證重要人物安全的同時也能讓他們不遭受財產損失?
請補充你的答案,感謝你的閱讀與關注,下一節再見~
閱讀延伸以及參考文檔
- Defer_statements
- Built-in_functions
- Go語言規格說明書 之 內建函數(Built-in functions)
- go語言快速入門:內建函數(6)
- 你知道defer的坑嗎?
- golang語言defer特性詳解.md
- Golang之輕鬆化解defer的溫柔陷阱