【新人系列】Golang 入門(八):defer 詳解 - 上

? 個人博客:https://blog.csdn.net/Newin2020?type=blog
📝 專欄地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 專欄定位:為 0 基礎剛入門 Golang 的小伙伴提供詳細的講解,也歡迎大佬們一起交流~
📚 專欄簡介:在這個專欄,我將帶著大家從 0 開始入門 Golang 的學習。在這個 Golang 的新人系列專欄下,將會總結 Golang 入門基礎的一些知識點,并由淺入深的學習這些知識點,方便大家快速入門學習~
?? 如果有收獲的話,歡迎點贊 👍 收藏 📁 關注,您的支持就是我創作的最大動力 💪

1. 快速了解

defer 后面的代碼會在函數 return 后執行,并且執行的順序是與代碼的順序相反,即倒序執行。

//main 2 1
func main() {defer fmt.Println("1")defer fmt.Println("2")fmt.Println("main")return
}

使用 defer 需要注意其執行的時機,以免造成意料之外的影響,例如它可能會修改返回值:

func deferReturn() (ret int) {defer func() {ret++}()return 10
}func main() {ret := deferReturn()fmt.Printf("ret = %d\r\n",ret)    //11
}

2. defer 執行邏輯

我們先來看一段簡潔的代碼。

func A() {defer B()//code to do something
}

上面這段代碼,編譯后的偽指令是下面這樣的。defer 指令對應到兩部分內容,其中 deferproc 負責把要執行的函數信息保存起來,我們稱之為 defer 注冊。而 deferproc 函數會返回 0,下面 if 分支和 panic recover 有關,可以先忽略不看,同時對應要跳轉的 ret 這里也先忽略不看。

func A() {r = deferproc(8, B)if r > 0 {goto ret}//code to do somethingruntime.deferreturn()return
ret:runtime.deferreturn()
}

去掉忽略的部分,程序的整體邏輯就比較清晰了。在 defer 注冊完成后,程序就會執行后面的邏輯,直到返回之前通過 deferreturn 執行注冊的 defer 函數,即 defer 調用。正是因為先注冊后調用,才實現了 defer 延遲執行的效果。

func A() {r = deferproc(8, B)    // 1.注冊//code to do somethingruntime.deferreturn()  // 2.調用return
}

看回 defer 注冊部分,defer 注冊的信息會注冊到一個鏈表,而當前執行的 goroutine 會持有這個鏈表的頭指針。每個 goroutine 在運行時都有一個對應的結構體 g,其中有一個字段就指向 defer 鏈表頭。

defer 鏈表鏈起來的是一個一個 _defer 結構體,新注冊的 defer 會添加到鏈表頭,執行時也是從頭開始,這也就是 defer 會表現為倒序執行的原因。

在這里插入圖片描述

在展開 _defer 結構之前,先看一個例子,這里函數 A 注冊了一個 defer 函數 A1。

func A1(a int) {fmt.Println(a)
}
func A() {a, b := 1, 2defer A1(a)a = a + bfmt.Println(a, b)
}

我們來看看函數調用棧,A 的棧幀首先會是存放兩個局部變量。接著 A1 只有一個參數,因此局部變量下面存放參數 a 的值 1,然后就要注冊 defer 函數 A1 了。

在這里插入圖片描述

deferproc 函數原型只有兩個參數,第一個參數是 defer 函數 A1 的參數加返回值共占多大空間。這里 A1 沒有返回值,只需要一個整形參數和一個指針變量,因此 64 位下要占 4 字節。

func deferproc (siz int32, fn *funcval)

第二個參數是一個 function value,前面函數部分我們也介紹過,沒有捕獲列表的 function value 在編譯階段就會做出優化,即在只讀數據段分配一個共用的 funcval 結構體,結構體中的指針會指向函數 A1 指令入口,所以 deferproc 的第二個參數就是結構體的地址 addr2。

func deferproc (siz = 4, fn = addr2)

在這里插入圖片描述

至此我們先把 _defer 的結構體展開了看一下:

type _defer struct {siz     int32     // 參數和返回值共占多少字節,這段空間會直接分配在_defer結構體后面,用于在注冊時保存參數,并在執行時拷貝到調用者參數與返回值空間started bool      // 標記defer是否已經執行sp      uintptr   // 記錄注冊這個defer的函數棧指針(調用者棧指針),函數可以通過它判斷自己注冊的defer是否已經執行完了pc      uintptr   // deferproc的返回地址fn      *funcval  // 注冊的function value函數_panic  *_paniclink    *_defer   // 鏈接到前一個注冊的defer結構體
}

當 deferproc 函數調用時,編譯器會在后面繼續開辟一段空間,用于存放 defer 函數的返回值和參數,由于在這個例子里沒有返回值,因此只分配 defer 函數的一個參數的空間,這一段空間會被直接拷貝到 _defer 結構體的后面。

另外,返回值地址和調用者函數的 BP 則放在 deferproc 兩個參數之后。

在這里插入圖片描述

在 deferproc 函數執行時,需要堆分配一段空間用于存放 _defer 結構體,而在 _defer 結構體后面也會分配一段空間用于存放 siz 大小的參數與返回值,這里由于沒有返回值因此存放參數 a。(注意這里所有的變量存放的順序是從下至上的,因此參數 a 雖然說是存放在 _defer 結構體的后面,但其實分配的空間在該結構體存放的位置之上)

在這里插入圖片描述

然后這個 _defer 結構體就會被添加到 defer 鏈表頭,至此 deferproc 注冊結束。

_defer 結構體預分配
實際上 go 語言會預分配不同規格的 defer 池,執行時從空閑的 _defer 中取一個出來用即可。如果沒有空閑的或者沒有大小合適的,則會再進行堆分配,用完以后再放回空閑的 _defer 池,這樣就可以避免頻繁地堆分配與回收。

讓我們再回到函數代碼的執行,當代碼執行到函數 A 中的 a = a + b 這行代碼時,變量 a 被賦值為 3,然后下一步會輸出局部變量 a 和 b 的值,即 3 和 2。

在這里插入圖片描述

接下來就到 deferreturn 執行 defer 鏈表了,此時會從當前 goroutine 拿到鏈表頭上的這個 _defer 結構體,通過 _defer 結構體里的 fn = addr2 找到對應的 funcval,然后通過 funcval 中的 fn 可以拿到函數入口的地址 addr1。

在調用 A1 時,會把 _defer 后面的參數與返回值整個拷貝到 A1 的調用者棧上,然后 A1 開始執行,此時就會輸出 1。

這里的關鍵是 defer 函數的參數在注冊時拷貝到堆上,執行時又拷貝到棧上。并不會去使用到 A 函數棧中保存的局部變量 a 的值 3,所以即使在 defer 函數注冊后修改了這個局部變量 a 的值,也不會影響到執行 defer 函數時用到的變量 a。

在這里插入圖片描述

既然 deferproc 注冊的是一個 function value,我們下面就來看看捕獲列表時是什么情況,變量 a 在 defer 函數注冊后進行修改是否能影響到 defer 函數里使用的變量。

3. defer + 閉包

在下面這個例子中,defer 函數不止要傳遞局部變量 b 做參數,還捕獲了外層函數的局部變量 a 并形成了閉包。

func A() {a, b := 1, 2defer func(b int) {a = a + bfmt.Println(a, b)}(b)a = a + bfmt.Println(a, b)
}

匿名函數會由編譯器按照 A_func1 這樣的形式命名。如下圖所示,假設這個閉包函數的指令入口地址為 addr1。

由于捕獲變量 a 除了初始化賦值外還被修改過,所以局部變量 a 改為堆分配,而棧上存儲它的地址。另外,還有一個局部變量 b 也要分配。

在這里插入圖片描述

然后創建閉包對象,堆分配一個 funcval 結構體,并且捕獲列表中存儲 a 的地址。

deferproc 執行時,_defer 結構體中的 fn 就是這個 funcval 結構體的起始地址。除此之外,還要拷貝參數 b 的值到 _defer 結構體的后面,然后把這個 _defer 結構體添加到 defer 鏈表頭。

在這里插入圖片描述

至此,deferproc 注冊結束。然后接著執行到 a = a + b 這行代碼,變量 a 被賦值為 3。而下一步就自然輸出 a 和 b 的變量值,即 3 和 2。

在這里插入圖片描述

接著就到 deferreturn 了,從 defer 鏈表頭拿到這個 defer 結構體,執行注冊的 defer 函數時,需要把參數 b 拷貝到棧上的參數空間。

另外,閉包函數也會通過寄存器存儲的 funcval 地址加上偏移,找到捕獲變量 a 的地址。

在這里插入圖片描述

當執行到 defer 函數 A_func1 里的 a = a + b 這行代碼時,此時的 a = 3 且 b = 2,所以 a 會被賦值為 5。因此,下一步將會輸出變量 a 和 b 的值,即 5 和 2。

在這里插入圖片描述

可以發現當變量 a 變成被捕獲的變量形成閉包后,在注冊完 defer 函數后修改變量 a 是可以影響到 defer 函數中使用的變量值的。這是因為此時的變量 a 發生了逃逸,不再分配到棧上而是分配到堆上,defer 函數的變量 a 最終將會從堆上獲取具體的值。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/74948.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/74948.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/74948.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

鴻蒙開發:了解Canvas繪制

前言 本文基于Api13 系統的組件無法滿足我們的需求,這種情況下就不得不自己自定義組件,除了自定義組合組件,拓展組件,還有一種方式,那就是完全的自繪制組件,這種情況,常見的場景有,比…

【Linux筆記】進程間通信——命名管道

🔥個人主頁🔥:孤寂大仙V 🌈收錄專欄🌈:Linux 🌹往期回顧🌹:【Linux筆記】進程間通信——匿名管道||進程池 🔖流水不爭,爭的是滔滔不 一、命名管道…

Spring項目中使用EasyExcel實現Excel 多 Sheet 導入導出功能(完整版)

Excel 多 Sheet 導入導出功能完整實現指南 一、環境依賴 1. Maven 依賴 <!-- EasyExcel --> <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version> </dependency>…

全流程剖析需求開發:打造極致貼合用戶的產品

全流程剖析需求開發&#xff1a;打造極致貼合用戶的產品 一、需求獲取&#xff08;一&#xff09;與用戶溝通1.面談2.問卷調查3.會議討論 &#xff08;二&#xff09;觀察用戶工作&#xff08;三&#xff09;收集現有文檔 二、需求分析&#xff08;一&#xff09;提煉關鍵需求&…

SQL語句及其應用(中)(DQL語句之單表查詢)

SQL語句的定義: 概述: 全稱叫 Structured Query Language, 結構化查詢語言, 主要是實現 用戶(程序員) 和 數據庫軟件(例如: MySQL, Oracle)之間交互用的. 分類: DDL: 數據定義語言, 主要是操作 數據庫, 數據表, 字段, 進行: 增刪改查(CURD) 涉及到的關鍵字: create, drop, …

5000元組裝一臺本地運行中、小模型主機,參考配置 (運行DeepSeek、Qwen)

5000元組裝一臺本地運行中、小模型主機&#xff0c;參考配置 &#xff08;運行DeepSeek、Qwen) 5000元中、小模型主機 DeepSeek、Qwen 各精度模型推薦啟動方式 模型名稱 參數量 精度 模型大小 推薦運行模式 DeepSeek R1 7b Q4 5 GB LM Studio純GPU 14b Q4 9 GB LM…

【新手初學】SQL注入getshell

一、引入 木馬介紹&#xff1a; 木馬其實就是一段程序&#xff0c;這個程序運行到目標主機上時&#xff0c;主要可以對目標進行遠程控制、盜取信息等功能&#xff0c;一般不會破壞目標主機&#xff0c;當然&#xff0c;這也看黑客是否想要搞破壞。 木馬類型&#xff1a; 按照功…

Containerd+Kubernetes搭建k8s集群

虛擬機環境設置&#xff0c;如果不是虛擬機可以忽略不看 1、安裝配置containerd 1.1 添加 Kubernetes 官方倉庫 安裝cri-tools的時候需要用到 cat > /etc/yum.repos.d/kubernetes.repo << EOF [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kub…

應用待機分組管控是啥

1. 應用待機群組是啥&#xff1f; Android 9 引入了一個新功能&#xff0c;叫應用待機群組。簡單來說&#xff0c;就是根據你最近使用應用的頻率和時間&#xff0c;系統會把應用分成不同的“群組”。每個群組的應用能用的系統資源不一樣&#xff0c;比如后臺任務、鬧鐘、網絡請…

C/C++后端開發面經

字節跳動 客戶端開發 實習 一面(50min) 自我介紹是否愿意轉語言,是否只愿意搞后端選一個項目來詳細談談HTTP和HTTPS有什么區別?談一下HTTPS加密的具體過程&#xff1a; 非對稱加密 對稱加密 證書認證的方式 非對稱加密是為了保證對稱密鑰的安全性。 對稱…

【第十三屆“泰迪杯”數據挖掘挑戰賽】【2025泰迪杯】A題解題全流程(持續更新)

【第十三屆“泰迪杯”數據挖掘挑戰賽】【2025泰迪杯】A題解題全流程-思路&#xff08;持續更新&#xff09; 寫在前面&#xff1a; 1、A題、C題將會持續更新&#xff0c;陸續更新發布文章 2、賽題交流咨詢Q群&#xff1a;1037590285 3、全家桶依舊包含&#xff1a; 代碼、…

如何讓 history 記錄命令執行時間?Linux/macOS 終端時間戳設置指南

引言:你真的會用 history 嗎? 有沒有遇到過這樣的情況:你想回顧某個重要命令的執行記錄,卻發現 history 只列出了命令序號和內容,根本沒有時間戳?這在運維排查、故障分析、甚至審計時都會帶來極大的不便。 想象一下,你在服務器上誤刪了某個文件,但不知道具體是幾點執…

Redis緩存異常場景深度解析:穿透、擊穿、雪崩及終極解決方案

一、引言 在高并發系統中&#xff0c;緩存承擔著流量洪峰的削峰填谷作用。然而當緩存層出現異常時&#xff0c;可能引發數據庫級聯崩潰&#xff0c;造成系統癱瘓。本文將深入剖析緩存穿透、緩存擊穿、緩存雪崩三大典型問題&#xff0c;并提供企業級解決方案。文章包含7種防御策…

Scala 之 正則

regex 函數提取 import scala.util.matching.Regex// 輸入表達式 val expression "[a#0, round(a#0, 0) AS round(a, 0)#1, abs(a#0) AS abs(a)#2, len(cast(a#0 as string)) AS len(a)#3]"// 定義一個正則表達式來提取函數名稱 val functionPattern: Regex &quo…

CI/CD-Jenkins安裝與應用

CI/CD-Jenkins安裝與應用 Docker安裝Jenkins docker-compose.yaml version: "3.8" # # 自定義網絡配置 # networks:cicd:driver: bridgeservices:jenkins:# 盡量使用新版本的Jenkins, 低版本的Jenkins的有些插件使用不了# jenkins/jenkins:lts-jdk17是長期支持版…

驗證Linux多進程時間片切換的程序

?? 一、軟件需求 在同時運行多個CPU密集型進程時&#xff0c;需采集以下統計信息&#xff1a; 當前運行在邏輯CPU上的進程ID每個進程的運行進度百分比 實驗程序設計要求&#xff1a; 1. 命令行參數 參數說明示例值n并發進程數量3total總運行時長&#xff08;毫秒&…

IvorySQL:兼容Oracle數據庫的開源PostgreSQL

今天給大家介紹一款基于 PostgreSQL 開發、兼容 Oracle 數據庫的國產開源關系型數據庫管理系統&#xff1a;IvorySQL。 IvorySQL 由商瀚高軟件提供支持&#xff0c;主要的功能特性包括&#xff1a; 完全兼容 PostgreSQL&#xff1a;IvorySQL 基于 PostgreSQL 內核開發&#xf…

樹莓派超全系列文檔--(13)如何使用raspi-config工具其二

如何使用raspi-config工具其二 raspi-configPerformance optionsOverclockGPU memoryOverlay file systemFan Localisation optionsLocaleTime zoneKeyboardWLAN country Advanced optionsExpand filesystemNetwork interface namesNetwork proxy settingsBoot orderBootloader…

QT音樂播放器(1):數據庫保存歌曲

實現功能&#xff1a;用數據庫保存本地導入和在線搜索的歌曲記錄 目錄 一. 保存本地添加的歌曲 1. 使用QSettings &#xff08;1&#xff09;在構造函數中&#xff0c;創建對象。 &#xff08;2&#xff09;在導入音樂槽函數中&#xff0c;保存新添加的文件路徑&#xff0c…

自動化發布工具CI/CD實踐Jenkins常用工具和插件的使用

1、安裝常用工具 名稱版本備注jdkjava8代碼打包所需git1.8.3.1maven3.6.3注意配置私服內容nvm0.39.3多Node.js環境管理工具Node.jsv14.18.0 / v16.17.1包管理工具yarn1.22.15包管理工具 1.1 安裝jdk Jenkins 需要使用java11 及以上&#xff0c;但是代碼打包依賴jdk8&#xff…