拒絕濫用golang defer機制


原文鏈接 : 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是有不小的性能損耗,且過度使用后也會導致邏輯變復雜。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM