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 個步驟:
- 執行到 “return i“語句時,取值當前 i 值,賦值給 test_unnamed 返回值,得到函式 test 的返回值 0(因為 test_unnamed 中只定義了 i,並未操作,i 保留成初始預設值)。
- 按照先進後出的方式,一次呼叫 defer 語句執行。
- 執行真正的 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 是有不小的效能損耗,且過度使用後也會導致邏輯變複雜。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 2. Go中defer使用注意事項Go
- [譯] part 29: golang deferGolang
- golang split需要注意的一個點Golang
- golang基礎–細說deferGolang
- 需要提醒你關於 golang 中 map 使用的幾點注意事項Golang
- Go語言中 defer 使用場景及注意事項,你是要注意的!Go
- golang的defer踩坑彙總Golang
- Golang switch case 的使用注意點Golang
- C++ 實現Golang裡的deferC++Golang
- 使用HTTP需要注意什麼?HTTP
- Golang 中 defer Close() 的潛在風險Golang
- Golang, 以 9 個簡短程式碼片段,弄懂 defer 的使用特點Golang
- GO 中的 defer 有哪些注意事項?上Go
- 使用 sendBeacon 需要注意的問題
- 使用FMEA需要注意些什麼?
- Golang中defer的三個實戰要點Golang
- Golang 高效實踐之defer、panic、recover實踐Golang
- Golang之輕鬆化解defer的溫柔陷阱Golang
- 記錄使用 Homestead 需要注意的事情
- Golang 中的 Defer 必掌握的 7 知識點Golang
- 簡單聊聊Golang中defer預計算引數Golang
- 使用 foreach 使用引用變數需要注意的問題變數
- Dockerfile小記之使用RUN命令需要注意Docker
- Golang研學:defer!如何掌握並用好(延遲執行)Golang
- 簡單探討Golang中defer預計算引數Golang
- 使用iceberg-使用Iceberg資料湖需要注意的點
- Golang陣列注意細節Golang陣列
- golang 中 channel 的詳細使用、使用注意事項及死鎖分析Golang
- Laravel 使用 PostgreSQL 資料庫需要注意的點LaravelSQL資料庫
- 使用代理IP抓取資料需要注意什麼?
- 關於使用vector時需要注意的細節
- php 實現golang defer延遲執行(先進後出)PHPGolang
- 使用MVCPager做AJAX分頁所需要注意的地方MVC
- 使用JavaScript變數需要注意哪些語法細節?JavaScript變數
- 電磁流量計在使用需要注意的問題
- Go 需要注意的坑Go
- Golang錯誤處理函式defer、panic、recover、errors.New介紹Golang函式Error
- Vue mixins淺談使用方法及需要注意的點Vue