defer概述
defer用來聲明一個延遲函數,把這個函數放入到一個棧上,當外部的包含方法return之前,返回參數到調用方法之前調用,也可以說是運行到最外層方法體時調用。我們經常用他來做一些資源的釋放,比如關閉io操作。
defer是golang的一個特色功能,被稱為“延遲調用函數”。當外部函數返回后執行defer。類似于其他語言的 try… catch … finally… 中的finally,當然差別還是明顯的。在使用defer之前我們應該多了解defer的特性,這樣才能避免使用上的誤區。
defer具體規則
1)延遲函數的參數在defer語句出現時就已經確定下來了:
```
package main
import "fmt"
func f(){
i:=0
defer fmt.Println(i)
i++
return
}
func main(){
f()
}
```
結果:
0
defer語句中的fmt.Println()參數i值在defer出現時就已經確定下來,實際上是拷貝了一份。后面對變量i的修改不會影響fmt.Println()函數的執行,仍然打印“0”.
注意:對于指針類型參數,規則仍然適用,只不過延遲函數的參數是一個地址值,這種情況下,defer后面的語句對變量的修改可能會影響延遲函數。
2)延遲函數執行按后進先出順序執行,即先出現的defer后執行
這個規則很好理解,定義defer類似于入棧操作,執行defer類似于出棧操作。設計defer的初衷是簡化函數返回時資源清理的動作,資源往往有依賴順序,比如先申請資源A,再根據A資源申請B資源,根據B資源申請C資源,即申請順序是:A-->B-->C,釋放時往往又要反向進行。這就是把deffer設計成FIFO的原因。
每申請到一個用完需要釋放的資源時,立即定義一個defer來釋放資源是個很好的習慣。
3)延遲函數可能操作主函數的具名返回值
定義defer的函數,即主函數可能有返回值,返回值沒有名字沒有關系,defer所作用的函數,即延遲函數可能會影響返回值。若要理解延遲函數是如何影響主函數返回值的,只要明白函數是如何返回的就足夠了。
4)函數返回過程
有一個事實必須要了解,關鍵字return不是一個原子操作,實際上return只代理匯編指令ret,即將跳轉程序執行。return i,實際上分兩步進行,即將i值存入棧中作為返回值,然后執行跳轉,而defer的執行時機正是跳轉前,所以說defer執行時還是有機會操作返回值的。
```
case1:
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
```
該函數的return語句可以拆分成下面兩行:
```
result = i
return
```
而延遲函數的執行正是在return之前,即加入defer后的執行過程如下:
```
result = i
result++
return
```
所以上面函數實際返回i++值。
關于上面函數實際返回i++
關于主函數有不同的返回方式,但返回機制就如上機介紹說,只要把return語句拆開都可以很好的理解,下面分別舉例說明
case2:主函數擁有匿名返回值,返回字面量
一個主函數擁有一個匿名的返回值,返回時使用字面量,比如返回“1”、“2”、“Hello”這樣的值,這種情況下語句時無法操作返回值的。一個返回字面值的函數,如下所示:
```
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
```
上面的return語句,直接把1寫入棧中作為返回值,延遲函數無法操作該返回值,所以就無法影響返回值。
case3:主函數擁有匿名返回值,返回變量
一個主函數擁有一個匿名的返回值,返回使用本地或局部變量,這種情況下,defer語句可以引用到返回值,但不會改變返回值。
```
func foo() int {
var i int
defer func() {
i++
}()
return i
}
```
上面的函數,返回一個局部變量,同時defer函數也會操作這個局部變量。對于匿名返回值來說,可以假定仍然有一個變量存儲返回值,假定返回值變量為“anony”,上面的返回語句可以拆分成一下過程:
```
anony=i
i++
return
```
由于i是整形,會將值拷貝給anony,所以defer語句中修改i值,對函數返回值不造成影響。
case4:主函數擁有具名返回值
主函數聲明語句中帶名字的返回值,會被初始化成一個局部變量,函數內部可以像使用局部變量一樣使用該返回值。如果defer語句操作該返回值,可能會改變返回結果。
一個影響函數返回的例子:
```
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
```
上面的函數拆解出來,如下所示:
```
ret = 0
ret++
return
```
函數真正返回前,在defer中對返回值做了+1操作,所以函數最終返回1
具體題目
題目1:
```
func deferFuncParameter() {
var aInt = 1
defer fmt.Println(aInt)
aInt = 2
return
}
```
輸出結果:1,延遲函數的參數在defer語句出現的時候就已經確定了,所以無論后面如何修改alnt變量都不會影響延遲函數。
題目2:
```
func main(){
deferFuncParameter()
}
func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(&aArray)
aArray[0] = 10
return
}
```
輸出:10 2 3.延遲函數的參數在defer語句出現時就已經確定了,即數組的地址,由于延遲函數執行時機是在return語句之前,所以對數組的最終修改值會被打印出來。
題目3:
```
func main(){
fmt.Println(deferFuncReturn())
}
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
```
輸出:2 。函數的return語句并不是原子的,實際執行分為設置返回值-->ret,defer語句實際執行在返回前,即擁有defer的函數返回過程是:設置返回值-->執行defer-->ret.所以return語句先把result設置為i的值,即1,defer語句中又把result遞增1,所以最終返回2.
defer的實現原理
1.defer數據結構
```
type _defer struct {
sp uintptr //函數棧指針
pc uintptr //程序計數器
fn *funcval //函數地址
link *_defer //指向自身結構的指針,用于鏈接多個defer
}
```
我們知道defer后面一定要接一個函數的,所以defer的數據結構根一般函數類似,也有棧指針、程序計數器、函數地址等等。
與函數不同的一點是它含有一個指針,可用于指向另一個defer,每個goroutine數據結構中實際上也有一個defer指針,該指針指向一個defer的鏈表,每次聲明一個defer時就將defer插入到單鏈表表頭,每次執行defer就從單鏈表表頭取出一個defer執行。

從上圖可以看到,新聲明的defer總是添加到鏈表頭部。
函數返回前執行defer則是從鏈表首部依次取出執行,不再贅述。
一個goroutine可能連續調用多個函數,defer添加過程跟上述流程一致,進入函數時添加defer,離開函數時取出defer,所以即便調用多個函數,也總是能保證defer是按FIFO方式執行的。
2.defer的創建和執行
源碼包src/runtime/panic.go定義了兩個方法分別用于創建defer和執行defer。
deferproc(): 在聲明defer處調用,將其defer函數存入goroutine的鏈表中;
deferreturn(): 在return指令,準確的講是在ret指令前調用,將其defer從goroutine鏈表中取出并執行
可以這么理解,在編譯階段,聲明defer處插入了函數deferproc(),在函數return前插入了函數deferreturn()
3.總結
defer定義的延遲函數參數在defer語句定義時就已經確定下來了;
defer定義順序與實際執行順序相反;
return不是原子操作,執行過程是:保存返回值(若有)-->執行defer(若有)-->執行ret跳轉
申請資源后立即使用defer關閉資源是好習慣
## defer 使用中的一些坑
### 坑1:defer在匿名返回值和命名返回值函數中的不同表現
* 匿名返回,執行 return 語句后,Go會創建一個臨時變量保存返回值,defer修改的是臨時變量,沒有修改返回值。
* 命名返回,執行 return 語句時,并不會再創建臨時變量保存,defer修改的是返回值。
### 坑2:在for循環中使用defer可能導致的性能問題
```
func deferInLoops() {
for i := 0; i < 100; i++ {
f, _ := os.Open("/etc/hosts")
defer f.Close()
}
}
```
defer在緊鄰創建資源的語句后執行,看上去邏輯沒有什么問題,但是和直接調用相比,defer的執行存在著額外的開銷,例如defer會對其后需要的參數進行內存拷貝,還需要對defer結構進行壓棧出棧操作。
??所以在循環中定義defer可能導致大量的資源開銷,在本例中,可以將f.Close()語句前的defer去掉,來減少大量defer導致的額外資源消耗。
坑3:判斷執行沒有err之后,再defer釋放資源
??一些獲取資源的操作可能會返回err參數,我們可以選擇忽略返回的err參數,但是如果要使用defer進行延遲釋放的話,需要在使用defer之前先判斷是否存在err,如果資源沒有獲取成功,即沒有必要也不應該再對資源執行釋放操作。如果不判斷獲取資源是否成功就執行釋放操作的話,還有可能導致釋放方法執行錯誤。
正確寫法:
```
resp, err := http.Get(url)
// 先判斷操作是否成功
if err != nil {
return err
}
// 如果操作成功,再進行Close操作
defer resp.Body.Close()
```
### 坑4:調用os.Exit時defer不會被執行
??當發生panic時,所在goroutine的所有defer會被執行,但是當調用os.Exit()方法退出程序時,defer并不會被執行。
```
func deferExit() {
defer func() {
fmt.Println("defer")
}()
os.Exit(0)
}
// defer并不會輸出
```
坑5:recover 不能跨協程捕獲 panic 信息
recover 必須在 defer 函數中使用,但是不能被 defer 直接調用;
多個 panic 僅有最后一個可以被 recover 捕獲,后面的panic 會覆蓋掉之前的;
recover 只能恢復同一個協程中的 panic ,不能跨協程捕獲 panic 信息。
- Golang
- Beego框架
- Gin框架
- gin框架介紹
- 使用Gin web框架的知名開源線上項目
- go-admin-gin
- air 熱啟動
- 完整的form表單參數驗證語法
- Go 語言入門練手項目推薦
- Golang是基于多線程模型
- golang 一些概念
- Golang程序開發注意事項
- fatal error: all goroutines are asleep - deadlock
- defer
- Golang 的內建調試器
- go部署
- golang指針重要性
- 包(golang)
- Golang框架選型比較: goframe, beego, iris和gin
- GoFrame
- golang-admin-項目
- go module的使用方法及原理
- go-admin支持多框架的后臺系統(go-admin.cn)
- docker gocv
- go-fac
- MSYS2
- 企業開發框架系統推薦
- gorm
- go-zero
- 優秀系統
- GinSkeleton(gin web 及gin 知識)
- 一次 request -> response 的生命周期概述
- 路由與路由組以及gin源碼學習
- 中間件以及gin源碼學習
- golang項目部署
- 獨立部署golang
- 代理部署golang
- 容器部署golang
- golang交叉編譯
- goravel
- kardianos+gin 項目作為windows服務運行
- go env
- 適用在Windows、Linux和macOS環境下打包Go應用程序的詳細步驟和命令
- Redis
- Dochub
- Docker部署開發go環境
- Docker部署運行go環境
- dochub說明
- Vue
- i18n
- vue3
- vue3基本知識
- element-plus 表格單選
- vue3后臺模板
- Thinkphp
- Casbin權限控制中間件
- 容器、依賴注入、門面、事件、中間件
- tp6問答
- 偽靜態
- thinkphp-queue
- think-throttle
- thinkphp隊列queue的一些使用說明,queue:work和queue:listen的區別
- ThinkPHP6之模型事件的觸發條件
- thinkphp-swoole
- save、update、insert 的區別
- Socket
- workerman
- 介紹
- 從ThinkPHP6移植到Webman的一些技術和經驗(干貨)
- swoole
- swoole介紹
- hyperf
- hf官網
- Swoft
- swoft官網
- easyswoole
- easyswoole官網地址
- EASYSWOOLE 聊天室DEMO
- socket問答
- MySQL
- 聚簇索引與非聚簇索引
- Mysql使用max獲取最大值細節
- 主從復制
- 隨機生成20萬User表的數據
- MySQL進階-----前綴索引、單例與聯合索引
- PHP
- 面向切面編程AOP
- php是單線程的一定程度上也可以看成是“多線程”
- PHP 線程,進程、并發、并行 的理解
- excel數據畫表格圖片
- php第三方包
- monolog/monolog
- league/glide
- 博客-知識網站
- php 常用bc函數
- PHP知識點的應用場景
- AOP(面向切面編程)
- 注解
- 依賴注入
- 事件機制
- phpspreadsheet導出數據和圖片到excel
- Hyperf
- mineAdmin
- 微服務
- nacos注冊服務
- simps-mqtt連接客戶端simps
- Linux
- 切換php版本
- Vim
- Laravel
- RabbitMQ
- thinkphp+rabbitmq
- 博客
- Webman框架
- 框架注意問題
- 關于內存泄漏
- 移動端自動化
- 懶人精靈
- 工具應用
- render
- gitlab Sourcetree
- ssh-agent失敗 錯誤代碼-1
- 資源網站
- Git
- wkhtmltopdf
- MSYS2 介紹
- powershell curl 使用教程
- NSSM(windows服務工具)
- MinGW64
- 知識擴展
- 對象存儲系統
- minio
- 雪花ID
- 請求body參數類型
- GraphQL
- js 深拷貝
- window 共享 centos文件夾
- 前端get/post 請求 特殊符號 “+”傳參數問題
- 什么是SCM系統?SCM系統與ERP系統有什么區別?
- nginx 日志格式統一為 json
- 特殊符號怎么打
- 收藏網址
- 收藏-golang
- 收藏-vue3
- 收藏-php
- 收藏-node
- 收藏-前端
- 規劃ITEM
- 旅游類
- 人臉識別
- dlib
- Docker&&部署
- Docker-compose
- Docker的網絡模式
- rancher
- DHorse
- Elasticsearch
- es與kibana都docke連接
- 4種數據同步到Elasticsearch方案
- GPT
- 推薦系統
- fastposter海報生成
- elasticsearch+logstash+kibana
- beego文檔系統-MinDoc
- jeecg開源平臺
- Java
- 打包部署
- spring boot
- 依賴
- Maven 相關 命令
- Gradle 相關命令
- mybatis
- mybatis.plus
- spring boot 模板引擎
- SpringBoot+Maven多模塊項目(創建、依賴、打包可執行jar包部署測試)完整流程
- Spring Cloud
- Sentinel
- nacos
- Apollo
- java推薦項目
- gradle
- Maven
- Nexus倉庫管理器
- Python
- Masonite框架
- scrapy
- Python2的pip2
- Python3 安裝 pip3
- 安全攻防
- 運維技術
- 騰訊云安全加固建議
- 免費freessl證書申請
- ruby
- homeland
- Protobuf
- GIT
- FFMPEG
- 命令說明
- 音頻
- ffmpeg合并多個MP4視頻
- NODEJS
- 開發npm包
- MongoDB
- php-docker-mongodb環境搭建
- mongo基本命令
- Docker安裝MongoDB最新版并連接
- 少兒編程官網
- UI推薦
- MQTT
- PHP連接mqtt
- EMQX服務端
- php搭建mqtt服務端