defer實現原理

2022-09-15 01:06:12 字數 4519 閱讀 6894

目錄4. defer實現原理

5. 應用

6. 總結

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

defer 函式所在的函式稱為主函式, defer語句關聯的函式稱為延遲函式

延遲函式可能有輸入引數,這些引數可能**於定義defer的函式,延遲函式也可能引用主函式用於返回的變數,也就是說延遲函式可能會影響主函式的一些行為。 類似與閉包。

defer 值拷貝

func deferfuncparameter()
輸出:

1
說明:延遲函式fmt.println(aint)的引數在defer語句出現時就已經確定了,所以無論後面如何修改aint變數都不會影響延遲函式。

defer時,相當於將aint 變數值拷貝了乙份兒傳遞進了defer延遲函式中

defer 傳遞主函式指標,會影響主函式的返回值

package main

import "fmt"

func printarray(array *[3]int)

}func deferfuncparameter()

defer printarray(&aarray)

aarray[0] = 10

return

}func main()

輸出:

10、2、3

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

延遲函式中操作主函式的返回值

func deferfuncreturn() (result int) ()

return i

}

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

延遲函式的引數在defer語句出現時就已經確定下來了

func a()
對於指標型別引數,規則仍然適用,只不過延遲函式的引數是乙個位址值,這種情況下,defer後面的語句對變數的修改可能會影響延遲函式

延遲函式執行按後進先出順序執行,即先出現的defer最後執行

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

每申請到乙個用完需要釋放的資源時,立即定義乙個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++值。

字面返回值示例:

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。

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

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

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

展示多個defer被鏈結的過程:

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

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

專案中, 有時為了讓程式更加健壯,即不使用panic, 通常使用recover來接受異常並處理

比如以下**:

func nopanic() 

}func dived(n int)

func nopanic()會自動接收異常,並列印相關日誌,算是乙個通用的異常處理函式。

業務處理函式中只要使用了defer nopanic(),那麼就不會再有panic發生

在專案中,有眾多的資料庫更新操作,正常的更新操作需要提交,而失敗的就需要回滾,如果異常分支比較多,

就會有很多重複的回滾**,所以有人嘗試了乙個做法:即在defer中判斷是否出現異常,有異常則回滾,否則提交

**簡化如下:

func ispanic() bool 

return false

}func updatetable() else

}()// database update operation...

}

func ispanic() bool用來接收異常,返回值用來說明是否發生了異常。func updatetable()函式中,使用defer來判斷最終應該提交還是回滾。

上面**初步看起來還算合理,但是此處的ispanic()再也不會返回true,不是ispanic()函式的問題,而是其呼叫的位置不對。

上面**ispanic失效了, 原因是違反了recover的乙個限制, 導致recover失效(永遠返回的是nil)

以下的三種條件會讓recover返回nil:

panic時指定引數為nil; (一般panic語句如 panic("*** failed"))

當前協程沒有發生panic;

所以recover()永遠返回nil

golang延遲函式defer

golang的defer優雅又簡潔,是golang的亮點之一。defer在宣告時不會立即執行,而是在函式return後,再按照先進後出的原則依次執行每個defer,一般用於釋放資源 清理資料 記錄日誌 異常處理等。下面舉個例子 package main import fmt func deferte...

go語言 defer 高階

go語言defer語句的用法 defer後面必須是函式呼叫語句,不能是其他語句,否則編譯器會出錯。package main import log func foo n int int func main 這個例子中defer後面使用的是n 指令,不是乙個函式呼叫語句,編譯器就報錯 command l...

defer詳解 結合例項

package main import fmt func main func foo int這段 執行後會列印出 in foo 1000 in defer 0 in main func 1024package main import fmt func main func foo int i 1000...