網站首頁 編程語言 正文
1. 簡介
defer 會在當前函數返回前執行傳入的函數,它會經常被用于關閉文件描述符、關閉數據庫連接以及解鎖資源。
理解這句話主要在三個方面:
- 當前函數
- 返回前執行,當然函數可能沒有返回值
- 傳入的函數,即 defer 關鍵值后面跟的是一個函數,包括普通函數如(fmt.Println), 也可以是匿名函數 func()
1.1 使用場景
使用 defer 的最常見場景是在函數調用結束后完成一些收尾工作,例如在 defer 中回滾數據庫的事務:
func createPost(db *gorm.DB) error { tx := db.Begin() // 用來回滾數據庫事件 defer tx.Rollback() if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil { return err } return tx.Commit().Error }
在使用數據庫事務時,我們可以使用上面的代碼在創建事務后就立刻調用 Rollback 保證事務一定會回滾。哪怕事務真的執行成功了,那么調用 tx.Commit() 之后再執行 tx.Rollback() 也不會影響已經提交的事務。
1.2 注意事項
使用defer
時會遇到兩個常見問題,這里會介紹具體的場景并分析這兩個現象背后的設計原理:
defer 關鍵字的調用時機以及多次調用 defer 時執行順序是如何確定的defer 關鍵字使用傳值的方式傳遞參數時會進行預計算,導致不符合預期的結果
作用域
向 defer 關鍵字傳入的函數會在函數返回之前運行。
假設我們在 for 循環中多次調用 defer 關鍵字:
package main import "fmt" func main() { for i := 0; i < 5; i++ { // FILO, 先進后出, 先出現的關鍵字defer會被壓入棧底,會最后取出執行 defer fmt.Println(i) } }
#運行
$ go run main.go
4
3
2
1
0
運行上述代碼會倒序執行傳入 defer 關鍵字的所有表達式,因為最后一次調用 defer 時傳入了 fmt.Println(4),所以這段代碼會優先打印 4。我們可以通過下面這個簡單例子強化對 defer 執行時機的理解:
package main import "fmt" func main() { // 代碼塊 { defer fmt.Println("defer runs") fmt.Println("block ends") } fmt.Println("main ends") }
# 輸出
$ go run main.go
block ends
main ends
defer runs
從上述代碼的輸出我們會發現,defer 傳入的函數不是在退出代碼塊的作用域時執行的,它只會在當前函數和方法返回之前被調用。
預計算參數
Go 語言中所有的函數調用都是傳值的.
雖然 defer 是關鍵字,但是也繼承了這個特性。假設我們想要計算 main 函數運行的時間,可能會寫出以下的代碼:
package main import ( "fmt" "time" ) func main() { startedAt := time.Now() // 這里誤以為:startedAt是在time.Sleep之后才會將參數傳遞給defer所在語句的函數中 defer fmt.Println(time.Since(startedAt)) time.Sleep(time.Second) }
# 輸出
$ go run main.go
0s
上述代碼的運行結果并不符合我們的預期,這個現象背后的原因是什么呢?
經過分析(或者使用debug方式),我們會發現:
調用 defer 關鍵字會立刻拷貝函數中引用的外部參數
所以 time.Since(startedAt) 的結果不是在 main 函數退出之前計算的,而是在 defer 關鍵字調用時計算的,最終導致上述代碼輸出 0s。
想要解決這個問題的方法非常簡單,我們只需要向 defer 關鍵字傳入匿名函數:
package main import ( "fmt" "time" ) func main() { startedAt := time.Now() // 使用匿名函數,傳遞的是函數的指針 defer func() { fmt.Println(time.Since(startedAt)) }() time.Sleep(time.Second) }
#輸出
$ go run main.go
$ 1.0056135s
2. defer 數據結構
defer 關鍵字在 Go 語言源代碼中對應的數據結構:
type _defer struct { siz int32 started bool openDefer bool sp uintptr pc uintptr fn *funcval _panic *_panic link *_defer }
簡單介紹一下 runtime._defer 結構體中的幾個字段:
- siz 是參數和結果的內存大小;
- sp 和 pc 分別代表棧指針和調用方的程序計數器;
- fn 是 defer 關鍵字中傳入的函數;
- _panic 是觸發延遲調用的結構體,可能為空;
- openDefer 表示當前 defer 是否經過開放編碼的優化;
除了上述的這些字段之外,runtime._defer 中還包含一些垃圾回收機制使用的字段, 這里不做過多的說明
3. 執行機制
堆分配、棧分配和開放編碼是處理 defer 關鍵字的三種方法。
- 早期的 Go 語言會在堆上分配, 不過性能較差
- Go 語言在 1.13 中引入棧上分配的結構體,減少了 30% 的額外開銷
- 在1.14 中引入了基于開放編碼的 defer,使得該關鍵字的額外開銷可以忽略不計
堆上分配暫時不做過多的說明
3.1 棧上分配
在 1.13 中對 defer 關鍵字進行了優化,當該關鍵字在函數體中最多執行一次時,會將結構體分配到棧上并調用。
除了分配位置的不同,棧上分配和堆上分配的 runtime._defer 并沒有本質的不同,而該方法可以適用于絕大多數的場景,與堆上分配的 runtime._defer 相比,該方法可以將 defer 關鍵字的額外開銷降低 ~30%。
3.2 開放編碼
在 1.14 中通過開放編碼(Open Coded)實現 defer 關鍵字,該設計使用代碼內聯優化 defer 關鍵的額外開銷并引入函數數據 funcdata 管理 panic 的調用3,該優化可以將 defer 的調用開銷從 1.13 版本的~35ns
降低至 ~6ns
左右:
然而開放編碼作為一種優化 defer 關鍵字的方法,它不是在所有的場景下都會開啟的,開放編碼只會在滿足以下的條件時啟用:
- 函數的 defer 數量小于或等于8個;
- 函數的 defer 關鍵字不能再循環中執行
- 函數的 return 語句 與 defer 語句個數的成績小于或者等于15個。
4. 參考
https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/
原文鏈接:https://www.cnblogs.com/failymao/p/15708844.html
相關推薦
- 2022-10-16 docker保存鏡像到本地并加載本地鏡像文件詳解_docker
- 2022-01-12 2022年了--你還不會手寫promise? --_-- promise的實現 第一版
- 2022-07-17 Docker?Push?Skipped?foreign?layer?的錯誤問題及解決方案_docke
- 2022-04-18 C語言復數的加減及輸出結構體_C 語言
- 2022-06-19 教你cmd?bat文件中調用另一個bat文件的方法_DOS/BAT
- 2023-01-18 你不知道的C++中namespace和using的用法實例_C 語言
- 2022-12-01 Entity?Framework使用DBContext實現增刪改查_實用技巧
- 2022-03-24 postman接口做關聯測試的方法步驟_相關技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支