defer
是一個用起來很是簡單的特性。
它的實現原理也不復雜。
本文主要介紹這個特性在實際項目中的利弊以及建議。
任何一個特性都有它的設計初衷,主要是被用來解決什麼問題的,任何一個特性也都有它合適和不合適出現的地方,咱們清楚地瞭解並正確合理地使用,是很是重要的。git
(注意:用 defer
固然確定比不用有必定的性能開銷,但咱們能夠忽略,由於影響確實很小。 換句話說,絕大部分狀況下,考慮是否使用 defer
時,性能開銷不該該是首先考慮的因素。可是!若是你的代碼是微秒級別的,那仍是要評估後再使用)github
幾乎全部其餘文章裏說 defer
如何如何有坑,defer
須要注意什麼等等。。都是官方文檔上講到的三點,在此就不贅述了。下面我分紅三部分,建議使用、中立和不建議。golang
defer
的設計初衷。defer func() { if r := recover(); r != nil { fmt.Println("Recovered", r) } }()
各類資源的使用,若是在用完以後不 close,就會形成資源的泄露,可能會嚴重影響程序運行,甚至形成程序死掉數據庫
c, err := Dial("udp", raddr) if err != nil { return err } defer c.Close()
f, err := os.Open(filename) if err != nil { return } defer f.Close()
fd, _ := os.Open("txt") errc := make(chan error, 1) // 主動關閉,減少 GC 壓力。 defer close(errc) var buf [1]byte n, err := fd.Read(buf[:1]) if n == 0 || err != nil { errc <- fmt.Errorf("read byte = %d, err = %v", n, err) }
type A struct { t int sync.Mutex } func main() { a := new(A) for i := 0; i < 2000; i++ { go a.incr() } time.Sleep(500 * time.Millisecond) // 此處用 sleep 簡單模擬等待同步,實際這樣寫不嚴謹,可用 waitGroup、channel 等 fmt.Println(a.t) } func (a *A) incr() { a.Lock() defer a.Unlock() // 模擬 ... 一堆邏輯 // 而後 ... 中間有好幾個 return 出口 // 若是咱們不用 defer,就要在每一個 return 都寫上 a.Unlock,否則就可能會形成死鎖 a.t++ }
這裏可能稍微有一些複雜,我稍微講一下
第一步,會先執行 log("do") 調用 log 函數傳入參數 「do」
第二步,log 函數執行函數體即 start := time.Now() fmt.Printf("enter %s\n", msg)
兩行,而後給調用方 do 函數返回一個 func()
第三步,這個 func() 被放到 defer 裏,等到 do 函數返回時纔會執行。安全
func main() { do() } func do() { defer log("do")() // ... 一些邏輯 time.Sleep(1 * time.Second) } func log(msg string) func() { start := time.Now() fmt.Printf("enter %s\n", msg) return func() { fmt.Printf("exit %s (%s)", msg, time.Since(start)) } }
由於 go 自帶的比較噁心的 err != nil 的判斷,業務邏輯中可能會有大量的這種代碼,而咱們又要對出錯進行一個統一的處理的時候,能夠用。網絡
tx, err := db.Begin() if err != nil { return err } defer func() { if err != nil { tx.Rollback() } }() // ... 中間會發生多個數據庫操做 ... // 提交,那麼在提交以前發生的任何錯誤,返回時均可利用以前註冊的 defer 進行回滾 tx.Commit()
不建議的用法就不給出代碼示例了,怕你看了錯誤的代碼示例反而記住了,就很差了。下面只說不建議的用法場景。函數
defer 是後定義的先執行,和棧相似。
若是在循環中調用 defer
,可能會致使堆積了不少 defer,在循環結束後纔會執行。
這中間若是有任何一個 defer
失敗了怎麼辦?
多個 defer
執行的內容有沒有依賴關係和衝突?
因此,除非萬不得已,不要給本身增長複雜度。
不這麼用就行了。性能
由於編譯器的不少優化對它都不起做用,因此儘可能不要傳入體積很大的參數,固然我以爲也應該沒有多少人會傳入一堆參數來用 defer
的。優化
由於 receiver
是當作第一個參數傳給調用函數的,也是值傳遞,除非你能時刻明確注意 receiver
是不是一個指針,不然最好不要用 defer
,否則可能沒法獲得你想要的結果。ui
defer
源碼實現的位置:runtime/panic.go
看到這知道我在建議使用中第一個就寫 recover
是爲何了吧。
這個特性最初的目的就是給 recover
用的。
編譯器會把 defer
關鍵字轉化爲對此函數的調用:
func deferproc(siz int32, fn *funcval)
而後當原函數 return
時,會調用:
func deferreturn(arg0 uintptr)
看,它只有一個參數,就是 arg0
,也就是 代碼中 defer
後面跟着的函數。明顯的,只有函數體自己會延遲執行,函數的參數在註冊 defer 以前就已經執行完了。
老老實實寫代碼,不要總想玩魔法。