Go defer實現原理剖析

2021-09-30 16:50:45 字數 4056 閱讀 4637

defer語句用於延遲函式的呼叫,每次defer都會把乙個函式壓入棧中,函式返回前再把延遲的函式取出並執行。

為了方便描述,我們把建立defer的函式稱為主函式,defer語句後面的函式稱為延遲函式。

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

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

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

下面函式輸出結果是什麼?

func deferfuncparameter()
題目說明:

函式deferfuncparameter()定義乙個整型變數並初始化為1,然後使用defer語句列印出變數值,最後修改變數值為2.

下面程式輸出什麼?

package main

import "fmt"

func printarray(array *[3]int)

}func deferfuncparameter()

defer printarray(&aarray)

aarray[0] = 10

return

}func main()

函式說明:

函式deferfuncparameter()定義乙個陣列,通過defer延遲函式printarray()的呼叫,最後修改陣列第乙個元素。printarray()函式接受陣列的指標並把陣列全部列印出來。

參***:

輸出10、2、3三個值。延遲函式printarray()的引數在defer語句出現時就已經確定了,即陣列的位址,由於延遲函式執行時機是在return語句之前,所以對陣列的最終修改值會被列印出來。

下面函式輸出什麼?

func deferfuncreturn() (result int) ()

return i

}

函式說明:

函式擁有乙個具名返回值result,函式內部宣告乙個變數i,defer指定乙個延遲函式,最後返回變數i。延遲函式中遞增result。

參***:

函式輸出2。函式的return語句並不是原子的,實際執行分為設定返回值-->ret,defer語句實際執行在返回前,即擁有defer的函式返回過程是:設定返回值-->執行defer-->ret。所以return語句先把result設定為i的值,即1,defer語句中又把result遞增1,所以最終返回2。

golang官方部落格裡總結了defer的行為規則,只有三條,我們圍繞這三條進行說明。

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

func a()
defer語句中的fmt.println()引數i值在defer出現時就已經確定下來,實際上是拷貝了乙份。後面對變數i的修改不會影響fmt.println()函式的執行,仍然列印"0"。

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

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

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

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

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

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

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

func deferfuncreturn() (result int) ()

return i

}

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

result = i

return

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

result = i

result++

return

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

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

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

乙個返回字面值的函式,如下所示:

func foo() int ()

return 1

}

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

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

乙個返回本地變數的函式,如下所示:

func foo() int ()

return i

}

上面的函式,返回乙個區域性變數,同時defer函式也會操作這個區域性變數。對於匿名返回值來說,可以假定仍然有乙個變數儲存返回值,假定返回值變數為"anony",上面的返回語句可以拆分成以下過程:

anony = i

i++return

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

主函宣告語句中帶名字的返回值,會被初始化成乙個區域性變數,函式內部可以像使用區域性變數一樣使用該返回值。如果defer語句操作該返回值,可能會改變返回結果。

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

func foo() (ret int) ()

return 0

}

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

ret = 0

ret++

return

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

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

原始碼包src/src/runtime/runtime2.go:_defer定義了defer的資料結構:

type _defer struct
我們知道defer後面一定要接乙個函式的,所以defer的資料結構跟一般函式類似,也有棧位址、程式計數器、函式位址等等。

與函式不同的一點是它含有乙個指標,可用於指向另乙個defer,每個goroutine資料結構中實際上也有乙個defer指標,該指標指向乙個defer的單鏈表,每次宣告乙個defer時就將defer插入到單鏈表表頭,每次執行defer時就從單鏈表表頭取出乙個defer執行。

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

從上圖可以看到,新宣告的defer總是新增到鍊錶頭部。

函式返回前執行defer則是從鍊錶首部依次取出執行,不再贅述。

乙個goroutine可能連續呼叫多個函式,defer新增過程跟上述流程一致,進入函式時新增defer,離開函式時取出defer,所以即便呼叫多個函式,也總是能保證defer是按fifo方式執行的。

原始碼包src/runtime/panic.go定義了兩個方法分別用於建立defer和執行defer。

可以簡單這麼理解,在編譯在階段,宣告defer處插入了函式deferproc(),在函式return前插入了函式deferreturn()。

Go slice實現原理剖析

slice又稱動態陣列,依託陣列實現,可以方便的進行擴容 傳遞等,實際使用中比陣列更靈活。正因為靈活,如果不了解其內部實現機制,有可能遭遇莫名的異常現象。slice的實現原理很簡單,本節試圖根據真實的使用場景,在原始碼中總結實現原理。按照慣例,我們開始前先看幾段 用於檢測對slice的理解程度。下面...

附近的人實現原理詳細剖析

要實現附近的人這個功能,我們要經歷以下幾個環節 使用者定時上傳自己的定位資訊,並存到服務端的資料庫中 使用者發起查詢請求,服務端根據使用者提供的定位資訊去資料庫中查詢與他的經度緯度海拔最接近的其他使用者的定位資訊。服務端通過兩個定位資訊就能算出距離,按照遠近排好序後,連同對應的使用者賬戶,暱稱,頭像...

GO 互斥鎖實現原理剖析

互斥鎖是併發程式中對共享資源進行訪問控制的主要手段,對此go語言提供了非常簡單易用的mutex,mutex為一結構體型別,對外暴露兩個方法lock 和unlock 分別用於加鎖和解鎖。mutex使用起來非常方便,但其內部實現卻複雜得多,這包括mutex的幾種狀態。另外,我們也想 一下mutex重複解...