Go 中 defer 的 5 個坑 - 第一部分

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。> 通過本節的學習以避免掉入基礎的 defer 陷阱中本文只適合想要進階學習 Golang 的新手閱讀,大牛請繞道。## #1 -- defer nil 函數如果一個延遲函數被賦值為 `nil` , 運行時的 [`panic`](https://golang.org/ref/spec#Handling_panics) 異常會發生在外圍函數執行結束後而不是 `defer` 的函數被調用的時候。例子```gofunc() { var run func() = nil defer run() fmt.Println("runs")}```輸出結果```runs️ panic: runtime error: invalid memory address or nil pointer dereference```### 發生了什嗎?名為 func 的函數一直運行至結束,然後 `defer` 函數會被執行且會因為值為 `nil` 而產生 `panic` 異常。然而值得注意的是,`run()` 的聲明是沒有問題,因為在外圍函數運行完成後它才會被調用。上面只是一個簡單的案例,但同樣的案例也可能發生在真實世界中,所以如果你遇上的話,可以想想是不是掉進了這個坑裡。## #2 -- 在迴圈中使用 defer切忌在迴圈中使用 `defer`,除非你清楚自己在做什麼,因為它們的執行結果常常會出人意料。但是,在某些情況下,在迴圈中使用 `defer` 會相當方便,例如將函數中的遞迴轉交給 `defer`,但這顯然已經不是本文應該講解的內容。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-1/defer_inside_a_loop.png)在上面的例子中,`defer row.Close()` 在迴圈中的延遲函數會在函數結束過後運行,而不是每次 for 迴圈結束之後。這些延遲函數會不停地堆積到延遲調用棧中,最終可能會導致一些不可預知的問題。### 解決方案 #1:不使用 `defer` ,直接在末尾調用。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-1/solution_1.png)### 解決方案 #2:將任務轉交給另一個函數然後在裡面使用 `defer`,在下面這種情況下,延遲函數會在每次匿名函數執行結束後執行。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-1/solution_2.png)### 進行基準測試![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-1/benchmark.jpg)[查看代碼](https://play.golang.org/p/GJ7oOMdBwJ)## #3 -- 延遲調用含有閉包的函數有時出於某種緣由,你想要讓那些閉包順延強制。例如,串連資料庫,然後在查詢語句執行過後中斷與資料庫的串連。例子```gotype database struct{}func (db *database) connect() (disconnect func()) { fmt.Println("connect") return func() { fmt.Println("disconnect") }}```運行一下```godb := &database{}defer db.connect() fmt.Println("query db...")```輸出結果```query db...connect```### 竟然出問題了?最終 `disconnect` 並沒有輸出,最後只有 `connect` ,這是一個 bug,最終的情況是 `connect()` 執行結束後,其執行域得以被儲存起來,但內部的閉包並不會被執行。### 解決方案```gofunc() { db := &database{} close := db.connect() defer close() fmt.Println("query db...")}```稍作修改後, `db.connect()` 返回了一個函數,然後我們再對這個函數使用 `defer` 就能夠在 `func()` 執行結束後斷開與資料庫的串連。輸出結果```connectquery db...disconnect```### 糟糕的處理方式:即便這種處理方式很糟,但我還是想告訴你如何不用變數來解決這個問題,因此,我希望你能以此來瞭解 defer 亦或是 go 語言的運行機制。```gofunc() { db := &database{} defer db.connect()() ..}```這段代碼從技術層面上說與上面的解決方案沒有本質區別。其中,第一個圓括弧是串連資料庫(在 `defer db.connect()` 中立即執行的部分),然後第二個圓括弧是為了在 `func()` 結束時順延強制中斷連線的函數(也就是返回的閉包)。歸因於 `db.connect()` 建立了一個閉包類型的值,然後再使用 `defer` 聲明閉包函數, `db.connect()` 的值需要被實現計算出來以便讓 `defer` 知道需要延遲哪個函數,這與 `defer` 不直接相關但也可能協助你解決一些問題。## #4 -- 在執行塊中使用 defer你可能想要在執行塊執行結束後執行在塊內延遲調用的函數,但事實並非如此,它們只會在塊所屬的函數執行結束後才被執行,這種情況適用於所有的代碼塊除了上文的函數塊例如,for,switch 等。**因為:延遲是相對於一個函數而非一個代碼塊**例子```gofunc main() { { defer func() { fmt.Println("block: defer runs") }() fmt.Println("block: ends") } fmt.Println("main: ends")}```輸出結果```block: endsmain: endsblock: defer runs```上例的延遲函數只會在函數執行結束後運行,而不是緊接著它所在的塊(花括弧內包含 defer 調用的地區)後執行,就像代碼中的示範的那樣,你可以使用花括弧創造單獨的執行塊。### 另一個解決方案如果你希望在另一個塊中使用 `defer` ,可以使用匿名函數(正如在第二個坑中我們採用的解決方案)。```gofunc main() { func() { defer func() { fmt.Println("func: defer runs") }() fmt.Println("func: ends") }() fmt.Println("main: ends")}```## #5 -- 延遲方法的坑同樣,你也可以使用 `defer` 來延遲 [方法](https://blog.learngoprogramming.com/go-functions-overview-anonymous-closures-higher-order-deferred-concurrent-6799008dde7b#61ec) 調用,但也可能出一些岔子。### 沒有使用指標作為接收者```gotype Car struct { model string}func (c Car) PrintModel() { fmt.Println(c.model)}func main() { c := Car{model: "DeLorean DMC-12"} defer c.PrintModel() c.model = "Chevrolet Impala"}```輸出結果```DeLorean DMC-12```### 使用指標對象作為接收者```gofunc (c *Car) PrintModel() { fmt.Println(c.model)}```輸出結果```Chevrolet Impala```### 為什麼會這樣?![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-1/what_is_going_on.png)我們需要記住的是,當外圍函數還沒有返回的時候,Go 的運行時就會立刻將傳遞給延遲函數的參數儲存起來。因此,當一個以值作為接收者的方法被 **defer** 修飾時,接收者會在聲明時被拷貝(在這個例子中那就是 *Car* 對象),此時任何對拷貝的修改都將不可見(例中的 *Car.model* ),因為,接收者也同時是輸入的參數,當使用 **defer** 修飾時會立刻得出參數的值(也就是 "DeLorean DMC-12" )。在另一種情況下,當被延遲調用時,接收者為指標對象,此時雖然會產生新的指標變數,但其指向的地址依然與上例中的 "c" 指標的地址相同。因此,任何修改都會完美地作用在同一個對象中。以上就是本文的全部內容,我會在後續的文章中補充更多類似的坑 -- 已經有至少 15 個易犯的 defer 錯誤榜上有名,如果你有任何想法,歡迎在下面留言。

via: https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01

作者:Inanc Gumus 譯者:yujiahaol68 校對:rxcai

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

3538 次點擊  ∙  1 贊  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.