Go Defer 高級實踐

defer 是一個用起來很是簡單的特性。
它的實現原理也不復雜。
本文主要介紹這個特性在實際項目中的利弊以及建議。

爲何要用 defer

任何一個特性都有它的設計初衷,主要是被用來解決什麼問題的,任何一個特性也都有它合適和不合適出現的地方,咱們清楚地瞭解並正確合理地使用,是很是重要的。git

優點

提升安全性、健壯性

讓代碼更優雅

劣勢

可讀性、可維護性

(注意:用 defer 固然確定比不用有必定的性能開銷,但咱們能夠忽略,由於影響確實很小。 換句話說,絕大部分狀況下,考慮是否使用 defer 時,性能開銷不該該是首先考慮的因素。可是!若是你的代碼是微秒級別的,那仍是要評估後再使用)github

defer 怎麼用

  1. 官方文檔,告訴你 defer 的基本用法
  2. 幾乎全部其餘文章裏說 defer 如何如何有坑,defer 須要注意什麼等等。。都是官方文檔上講到的三點,在此就不贅述了。下面我分紅三部分,建議使用、中立和不建議。golang

    • 建議使用 是官方 src 裏都在用的,並且也是 defer 的設計初衷。
    • 中立 是工程實踐中總結出來,平衡了代碼優雅和可讀性、可維護性後的結果。
    • 不建議 是弊大於利,得不償失的用法,主要影響的就是下降可讀性,可維護性。

建議使用

Recover

defer func() {
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}()

資源回收

各類資源的使用,若是在用完以後不 close,就會形成資源的泄露,可能會嚴重影響程序運行,甚至形成程序死掉數據庫

網絡 I/O
c, err := Dial("udp", raddr)
if err != nil {
    return err
}
defer c.Close()
文件 I/O
f, err := os.Open(filename)
if err != nil {
    return
}
defer f.Close()
channel 關閉
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 執行的內容有沒有依賴關係和衝突?
因此,除非萬不得已,不要給本身增長複雜度。
不這麼用就行了。性能

不要在 defer 中傳入體積很大的參數

由於編譯器的不少優化對它都不起做用,因此儘可能不要傳入體積很大的參數,固然我以爲也應該沒有多少人會傳入一堆參數來用 defer 的。優化

不要用 receiver 調用 defer

由於 receiver 是當作第一個參數傳給調用函數的,也是值傳遞,除非你能時刻明確注意 receiver 是不是一個指針,不然最好不要用 defer,否則可能沒法獲得你想要的結果。ui

未完待續。。。

defer 原理簡述

defer 源碼實現的位置:runtime/panic.go

看到這知道我在建議使用中第一個就寫 recover 是爲何了吧。
這個特性最初的目的就是給 recover 用的。

編譯器會把 defer 關鍵字轉化爲對此函數的調用:

func deferproc(siz int32, fn *funcval)

而後當原函數 return 時,會調用:

func deferreturn(arg0 uintptr)

看,它只有一個參數,就是 arg0,也就是 代碼中 defer 後面跟着的函數。明顯的,只有函數體自己會延遲執行,函數的參數在註冊 defer 以前就已經執行完了。

結語

老老實實寫代碼,不要總想玩魔法。

相關文章
相關標籤/搜索