【转】golang-defer坑的本质
本文節選自https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html
作者的分析非常透徹,從問題本質分析,就不會對defer產生的副作用產生迷茫。
defer坑的本質是:本質原因是return xxx語句并不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機會改變最終的返回值。
?
defer使用時的坑
?
先來看看幾個例子。
例1:
func f() (result int) {defer func() {result++}()return 0 }?
例2:
func f() (r int) {t := 5defer func() {t = t + 5}()return t }?
例3:
func f() (r int) {defer func(r int) {r = r + 5}(r)return 1 }請讀者先不要運行代碼,在心里跑一遍結果,然后去驗證。
例1的正確答案不是0,例2的正確答案不是10,如果例3的正確答案不是6......
defer是在return之前執行的。這個在?官方文檔中是明確說明了的。要使用defer時不踩坑,最重要的一點就是要明白,return xxx這一條語句并不是一條原子指令!
函數返回的過程是這樣的:先給返回值賦值,然后調用defer表達式,最后才是返回到調用函數中。
defer表達式可能會在設置函數返回值之后,在返回到調用函數之前,修改返回值,使最終的函數返回值與你想象的不一致。
其實使用defer時,用一個簡單的轉換規則改寫一下,就不會迷糊了。改寫規則是將return語句拆成兩句寫,return xxx會被改寫成:
返回值 = xxx 調用defer函數 空的return先看例1,它可以改寫成這樣:
func f() (result int) {result = 0 //return語句不是一條原子調用,return xxx其實是賦值+ret指令func() { //defer被插入到return之前執行,也就是賦返回值和ret指令之間result++}()return }所以這個返回值是1。
再看例2,它可以改寫成這樣:
func f() (r int) {t := 5r = t //賦值指令func() { //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過t = t + 5}return //空的return指令 }所以這個的結果是5。
最后看例3,它改寫后變成:
func f() (r int) {r = 1 //給返回值賦值func(r int) { //這里改的r是傳值傳進去的r,不會改變要返回的那個r值r = r + 5}(r)return //空的return }所以這個例子的結果是1。
defer確實是在return之前調用的。但表現形式上卻可能不像。本質原因是return xxx語句并不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機會改變最終的返回值。
defer的實現
defer關鍵字的實現跟go關鍵字很類似,不同的是它調用的是runtime.deferproc而不是runtime.newproc。
在defer出現的地方,插入了指令call runtime.deferproc,然后在函數返回之前的地方,插入指令call runtime.deferreturn。
普通的函數返回時,匯編代碼類似:
add xx SP return如果其中包含了defer語句,則匯編代碼是:
call runtime.deferreturn, add xx SP returngoroutine的控制結構中,有一張表記錄defer,調用runtime.deferproc時會將需要defer的表達式記錄在表中,而在調用runtime.deferreturn的時候,則會依次從defer表中出棧并執行。
?
?
------------------------------------------------
我在補充一下,go 的函數 帶參數名的返回值和不帶參數名的函數返回值對defer是有影響的。
如果defer 操作的是帶參數的函數返回值的參數名,則會直接影響到函數的返回值;
如果defer操作的是函數內部的一個局部變量,這不會影響到函數的返回值;
?
原因很簡單,return會做兩個事情,
1、拷貝返回值到函數的返回值內存區,有如下幾個場景:
- a.如果是函數的返回值帶參數名(假設參數名為a),實際在函數內部對a的操作是直接操作這個返回值的內存區域;
如果函數返回直接調用return,不帶任何返回值, 則不會有拷貝過程。此時defer如果沒有顯示的操作a,則不會影響到函數的返回值。
如果函數返回直接調用return X,則會有個a=x的拷貝過程,此時defer如果沒有顯示的操作a,也不會影響到函數的返回值(即使操作了x也不會影響結果)。
- b如果是函數的返回值不帶參名,函數的返回值需要一個顯示的return x語句,此時會有一個拷貝過程,就是將x的值拷貝到返回值的內存區域,此時defer操作x 不會有什么副作用,此時存放返回值的內存區域是個匿名區域,從源程序的角度看defer直接操作變量是不可能訪問到這個區域。可見不帶形參的返回是避免defer副作用的最有效手段。
2、執行RET指令,執行跳轉。
?
talk is cheep ,show me the code , 上一段代碼:
[csharp]?view plain?copy ?結果: [csharp]?view plain?copy ?
結論:為了避免defer可能引發的歧義,在定義函數時,最好使用不帶參數名的方式。
?
引申一下,我們來看下go函數的棧幀結構,首先學習下幾個寄存器
?
1.go的函數調用現場清理由主調函數負責維護; 2.函數的返回地址也在主調函數的棧上開辟; 3.棧上的數據是通過FB+- 偏移量來操作; 4.return語句分兩部分執行: 把返回值拷貝到返回值的棧上空間內 調用RET指令執行函數調用后的下一條指令 但是return的語句不是原子的,defer語句的執行被插入在這兩條指令執行之間,這是導致defer導致歧義的最根本原因。為避免困惑, defer 不應該操作函數返回值存放的區域,即defer語句里面不應該有對命名返回值參數的操作。 ? ?轉載于:https://www.cnblogs.com/ralap7/p/9194734.html
總結
以上是生活随笔為你收集整理的【转】golang-defer坑的本质的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 光大信用卡积分有效期
- 下一篇: 行业板块和概念板块有什么区别