golang defer使用需要注意

pert發表於2018-05-24

原文連結 : http://www.bugclosed.com/post/17

defer 機制

go 語言中的 defer 提供了在函式返回前執行操作的機制,在需要資源回收的場景非常方便易用(比如檔案關閉,socket 連結資源十分,資料庫回話關閉回收等),在定義資源的地方就可以設定好資源的操作,程式碼放在一起,減小忘記引起記憶體洩漏的可能。 defer 機制雖然好用,但卻不是免費的,首先效能會比直接函式呼叫差很多;其次,defer 機制中返回值求值也是一個容易出錯的地方。

一個簡單的效能對比測試

通過一個對鎖機制的 defer 操作來比較效能差異。

package main

import (
    "sync"
    "testing"
)

var (
    lock = new(sync.Mutex)
)

func lockTest() {
    lock.Lock()
    lock.Unlock()
}
func lockDeferTest() {
    lock.Lock()
    defer lock.Unlock()
}
func BenchmarkTest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        lockTest()
    }
}
func BenchmarkTestDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        lockDeferTest()
    }
}

執行命令 go test -v -test.bench, 效能對比測試結果如下:

BenchmarkTest-8         100000000           18.5 ns/op
BenchmarkTestDefer-8    20000000            56.4 ns/op

從測試結果可以看出,Defer 版本的 lock 操作時間消耗幾乎是函式直接呼叫的 3 倍以上。

defer 執行順序和返回值求值

看一個簡單的測試:

package main

import (
    "fmt"
)

func test_unnamed()(int) {
    var i int
    defer func() {
        i++
        fmt.Println("defer a:", i) 
    }()
    defer func() {
        i++
        fmt.Println("defer b :", i) 
    }()
    return i
}
func test_named()(i int) {
    defer func() {
        i++
        fmt.Println("defer c:", i) 
    }()
    defer func() {
        i++
        fmt.Println("defer d :", i) 
    }()
    return i
}

func main() {
    fmt.Println("return:", test_unnamed()) 
    fmt.Println("return:", test_named()) 
}

執行結果是:

defer b : 1
defer a: 2
return: 0
defer d : 1
defer c: 2
return: 2

關於同時有多個 defer 時的執行順序,可以看做是 go 編譯器為每個函式維護了一個先進後出的堆疊。每次遇到 defer 語句就講執行體封裝後壓入堆疊中,等到函式返回時,從堆疊中依次出棧執行。所以 “defer b” 語句在後,卻先呼叫。

關於函式求值問題,可以將 test_unnamed 函式返回和 defer 的執行和求值理解為 3 個步驟:

  1. 執行到 “return i“語句時,取值當前 i 值,賦值給 test_unnamed 返回值,得到函式 test 的返回值 0(因為 test_unnamed 中只定義了 i,並未操作,i 保留成初始預設值)。
  2. 按照先進後出的方式,一次呼叫 defer 語句執行。
  3. 執行真正的 test_unnamed 函式返回 ” return“。

以上是分析了匿名返回值的情況,具名返回值 test_named 的情況稍有不同,return 返回了 2,而不是 0,因為 defer 函式中對返回值變數 i 做了修改。

由此可見,使用多個 defer 和 defer 函式中還需要處理返回值的情況下極容易出問題,使用時需要小心謹慎。

defer 釋放鎖

通過 defer 釋放鎖(sync.Mutex) 是很常見的場景,示例如下:

def GetMapData(key uint32) uint32{
    lock.Lock()
    defer lock.Unlock()
    if v, ok := mapData[key]; ok{
        return v
    }
    return 0
}

在這樣簡單的場景下,通過 defer 直接釋放鎖,在後續的程式碼邏輯基本可以忘記鎖的存在而寫程式碼。但是這種模式就存在一個鎖粒度的問題--整個函式都被鎖住了。

如果 lock 後面還有很多複雜或者阻塞的邏輯(寫日誌,訪問資料庫,從 ch 讀取資料等),會導致鎖的持有時間過大,影響系統的處理效能;此時可以精細控制邏輯函式的分拆,讓鎖儘量只控制共享資源,拋棄 defer 自行控制 unlock,以免鎖粒度過大。

總結

defer 是一個很強大的機制,尤其是在資源釋放的場景特別適用。但是使用時要注意,defer 是有不小的效能損耗,且過度使用後也會導致邏輯變複雜。

更多原創文章乾貨分享,請關注公眾號
  • golang defer使用需要注意
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章