久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

Go defer的實現原理剖析

179次閱讀
沒有評論

共計 4297 個字符,預計需要花費 11 分鐘才能閱讀完成。

本篇內容介紹了“Go defer 的實現原理剖析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓丸趣 TV 小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

1. 前言

defer 語句用于延遲函數的調用,每次 defer 都會把一個函數壓入棧中,函數返回前再把延遲的函數取出并執行。

為了方便描述,我們把創建 defer 的函數稱為主函數,defer 語句后面的函數稱為延遲函數。

延遲函數可能有輸入參數,這些參數可能來源于定義 defer 的函數,延遲函數也可能引用主函數用于返回的變量,也就是說延遲函數可能會影響主函數的一些行為,這些場景下,如果不了解 defer 的規則很容易出錯。

其實官方說明的 defer 的三個原則很清楚,本節試圖匯總 defer 的使用場景并做簡單說明。

2. 熱身

按照慣例,我們看幾個有意思的題目,用于檢驗對 defer 的了解程度。

2.1 題目一

下面函數輸出結果是什么?

func deferFuncParameter() { var aInt = 1
 defer fmt.Println(aInt)
 aInt = 2
 return}

題目說明:
函數 deferFuncParameter() 定義一個整型變量并初始化為 1,然后使用 defer 語句打印出變量值,最后修改變量值為 2.

參考答案:
輸出 1。延遲函數 fmt.Println(aInt) 的參數在 defer 語句出現時就已經確定了,所以無論后面如何修改 aInt 變量都不會影響延遲函數。

2.2 題目二

下面程序輸出什么?

package mainimport  fmt 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}func main() { deferFuncParameter()
}

函數說明:
函數 deferFuncParameter() 定義一個數組,通過 defer 延遲函數 printArray() 的調用,最后修改數組第一個元素。printArray() 函數接受數組的指針并把數組全部打印出來。

參考答案:
輸出 10、2、3 三個值。延遲函數 printArray() 的參數在 defer 語句出現時就已經確定了,即數組的地址,由于延遲函數執行時機是在 return 語句之前,所以對數組的最終修改值會被打印出來。

2.3 題目三

下面函數輸出什么?

func deferFuncReturn() (result int) { i := 1
 defer func() {
 result++
 }() return i}

函數說明:
函數擁有一個具名返回值 result,函數內部聲明一個變量 i,defer 指定一個延遲函數,最后返回變量 i。延遲函數中遞增 result。

參考答案:
函數輸出 2。函數的 return 語句并不是原子的,實際執行分為設置返回值 – ret,defer 語句實際執行在返回前,即擁有 defer 的函數返回過程是:設置返回值 – 執行 defer– ret。所以 return 語句先把 result 設置為 i 的值,即 1,defer 語句中又把 result 遞增 1,所以最終返回 2。

3. defer 規則

Golang 官方博客里總結了 defer 的行為規則,只有三條,我們圍繞這三條進行說明。

3.1 規則一:延遲函數的參數在 defer 語句出現時就已經確定下來了

官方給出一個例子,如下所示:

func a() { i := 0
 defer fmt.Println(i)
 i++
 return
}

defer 語句中的 fmt.Println() 參數 i 值在 defer 出現時就已經確定下來,實際上是拷貝了一份。后面對變量 i 的修改不會影響 fmt.Println() 函數的執行,仍然打印 0。

注意:對于指針類型參數,規則仍然適用,只不過延遲函數的參數是一個地址值,這種情況下,defer 后面的語句對變量的修改可能會影響延遲函數。

3.2 規則二:延遲函數執行按后進先出順序執行,即先出現的 defer 最后執行

這個規則很好理解,定義 defer 類似于入棧操作,執行 defer 類似于出棧操作。

設計 defer 的初衷是簡化函數返回時資源清理的動作,資源往往有依賴順序,比如先申請 A 資源,再跟據 A 資源申請 B 資源,跟據 B 資源申請 C 資源,即申請順序是:A– B– C,釋放時往往又要反向進行。這就是把 deffer 設計成 FIFO 的原因。

每申請到一個用完需要釋放的資源時,立即定義一個 defer 來釋放資源是個很好的習慣。

3.3 規則三:延遲函數可能操作主函數的具名返回值

定義 defer 的函數,即主函數可能有返回值,返回值有沒有名字沒有關系,defer 所作用的函數,即延遲函數可能會影響到返回值。

若要理解延遲函數是如何影響主函數返回值的,只要明白函數是如何返回的就足夠了。

3.3.1 函數返回過程

有一個事實必須要了解,關鍵字 return 不是一個原子操作,實際上 return 只代理匯編指令 ret,即將跳轉程序執行。比如語句 return i,實際上分兩步進行,即將 i 值存入棧中作為返回值,然后執行跳轉,而 defer 的執行時機正是跳轉前,所以說 defer 執行時還是有機會操作返回值的。

舉個實際的例子進行說明這個過程:

func deferFuncReturn() (result int) { i := 1
 defer func() {
 result++
 }() return i}

該函數的 return 語句可以拆分成下面兩行:

result = i
return

而延遲函數的執行正是在 return 之前,即加入 defer 后的執行過程如下:

result = i
result++
return

所以上面函數實際返回 i ++ 值。

關于主函數有不同的返回方式,但返回機制就如上機介紹所說,只要把 return 語句拆開都可以很好的理解,下面分別舉例說明

3.3.1 主函數擁有匿名返回值,返回字面值

一個主函數擁有一個匿名的返回值,返回時使用字面值,比如返回 1、2、Hello 這樣的值,這種情況下 defer 語句是無法操作返回值的。

一個返回字面值的函數,如下所示:

func foo() int { var i int
 defer func() {
 i++
 }() return 1}

上面的 return 語句,直接把 1 寫入棧中作為返回值,延遲函數無法操作該返回值,所以就無法影響返回值。

3.3.2 主函數擁有匿名返回值,返回變量

一個主函數擁有一個匿名的返回值,返回使用本地或全局變量,這種情況下 defer 語句可以引用到返回值,但不會改變返回值。

一個返回本地變量的函數,如下所示:

func foo() int { var i int
 defer func() {
 i++
 }() return i}

上面的函數,返回一個局部變量,同時 defer 函數也會操作這個局部變量。對于匿名返回值來說,可以假定仍然有一個變量存儲返回值,假定返回值變量為 anony,上面的返回語句可以拆分成以下過程:

anony = i
return

由于 i 是整型,會將值拷貝給 anony,所以 defer 語句中修改 i 值,對函數返回值不造成影響。

3.3.3 主函數擁有具名返回值

主函聲明語句中帶名字的返回值,會被初始化成一個局部變量,函數內部可以像使用局部變量一樣使用該返回值。如果 defer 語句操作該返回值,可能會改變返回結果。

一個影響函返回值的例子:

func foo() (ret int) { defer func() {
 ret++
 }() return 0}

上面的函數拆解出來,如下所示:

ret = 0
ret++
return

函數真正返回前,在 defer 中對返回值做了 + 1 操作,所以函數最終返回 1。

4. defer 實現原理

本節我們嘗試了解一些 defer 的實現機制。

4.1 defer 數據結構

源碼包 src/src/runtime/runtime2.go:_defer 定義了 defer 的數據結構:

type _defer struct {
 sp uintptr // 函數棧指針
 pc uintptr // 程序計數器
 fn *funcval // 函數地址
 link *_defer // 指向自身結構的指針,用于鏈接多個 defer}

我們知道 defer 后面一定要接一個函數的,所以 defer 的數據結構跟一般函數類似,也有棧地址、程序計數器、函數地址等等。

與函數不同的一點是它含有一個指針,可用于指向另一個 defer,每個 goroutine 數據結構中實際上也有一個 defer 指針,該指針指向一個 defer 的單鏈表,每次聲明一個 defer 時就將 defer 插入到單鏈表表頭,每次執行 defer 時就從單鏈表表頭取出一個 defer 執行。

下圖展示一個 goroutine 定義多個 defer 時的場景: 

從上圖可以看到,新聲明的 defer 總是添加到鏈表頭部。

函數返回前執行 defer 則是從鏈表首部依次取出執行,不再贅述。

一個 goroutine 可能連續調用多個函數,defer 添加過程跟上述流程一致,進入函數時添加 defer,離開函數時取出 defer,所以即便調用多個函數,也總是能保證 defer 是按 FIFO 方式執行的。

4.2 defer 的創建和執行

源碼包 src/runtime/panic.go 定義了兩個方法分別用于創建 defer 和執行 defer。

deferproc():在聲明 defer 處調用,其將 defer 函數存入 goroutine 的鏈表中;

deferreturn():在 return 指令,準確的講是在 ret 指令前調用,其將 defer 從 goroutine 鏈表中取出并執行。

可以簡單這么理解,在編譯在階段,聲明 defer 處插入了函數 deferproc(),在函數 return 前插入了函數 deferreturn()。

5. 總結

defer 定義的延遲函數參數在 defer 語句出時就已經確定下來了

defer 定義順序與實際執行順序相反

return 不是原子操作,執行過程是: 保存返回值 (若有)– 執行 defer(若有)– 執行 ret 跳轉

申請資源后立即使用 defer 關閉資源是好習慣

“Go defer 的實現原理剖析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注丸趣 TV 網站,丸趣 TV 小編將為大家輸出更多高質量的實用文章!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-07-28發表,共計4297字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 于田县| 牙克石市| 崇仁县| 鹤壁市| 剑阁县| 塘沽区| 涞源县| 麻江县| 灵川县| 进贤县| 神池县| 清水县| 上杭县| 临沧市| 荆州市| 武邑县| 治县。| 湟中县| 赣榆县| 泸定县| 油尖旺区| 进贤县| 五莲县| 两当县| 行唐县| 芮城县| 大城县| 新巴尔虎左旗| 保康县| 汶上县| 沁源县| 卢湾区| 克什克腾旗| 宁津县| 铁岭县| 青川县| 卓资县| 博罗县| 中阳县| 米易县| 哈尔滨市|