日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

【转】golang-defer坑的本质

發(fā)布時(shí)間:2024/10/12 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【转】golang-defer坑的本质 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文節(jié)選自https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html

作者的分析非常透徹,從問(wèn)題本質(zhì)分析,就不會(huì)對(duì)defer產(chǎn)生的副作用產(chǎn)生迷茫。

defer坑的本質(zhì)是:本質(zhì)原因是return xxx語(yǔ)句并不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機(jī)會(huì)改變最終的返回值。

?

defer使用時(shí)的坑

?

先來(lái)看看幾個(gè)例子。

例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 }

請(qǐng)讀者先不要運(yùn)行代碼,在心里跑一遍結(jié)果,然后去驗(yàn)證。

例1的正確答案不是0,例2的正確答案不是10,如果例3的正確答案不是6......

defer是在return之前執(zhí)行的。這個(gè)在?官方文檔中是明確說(shuō)明了的。要使用defer時(shí)不踩坑,最重要的一點(diǎn)就是要明白,return xxx這一條語(yǔ)句并不是一條原子指令!

函數(shù)返回的過(guò)程是這樣的:先給返回值賦值,然后調(diào)用defer表達(dá)式,最后才是返回到調(diào)用函數(shù)中

defer表達(dá)式可能會(huì)在設(shè)置函數(shù)返回值之后,在返回到調(diào)用函數(shù)之前,修改返回值,使最終的函數(shù)返回值與你想象的不一致。

其實(shí)使用defer時(shí),用一個(gè)簡(jiǎn)單的轉(zhuǎn)換規(guī)則改寫(xiě)一下,就不會(huì)迷糊了。改寫(xiě)規(guī)則是將return語(yǔ)句拆成兩句寫(xiě),return xxx會(huì)被改寫(xiě)成:

返回值 = xxx 調(diào)用defer函數(shù) 空的return

先看例1,它可以改寫(xiě)成這樣:

func f() (result int) {result = 0 //return語(yǔ)句不是一條原子調(diào)用,return xxx其實(shí)是賦值+ret指令func() { //defer被插入到return之前執(zhí)行,也就是賦返回值和ret指令之間result++}()return }

所以這個(gè)返回值是1。

再看例2,它可以改寫(xiě)成這樣:

func f() (r int) {t := 5r = t //賦值指令func() { //defer被插入到賦值與返回之間執(zhí)行,這個(gè)例子中返回值r沒(méi)被修改過(guò)t = t + 5}return //空的return指令 }

所以這個(gè)的結(jié)果是5。

最后看例3,它改寫(xiě)后變成:

func f() (r int) {r = 1 //給返回值賦值func(r int) { //這里改的r是傳值傳進(jìn)去的r,不會(huì)改變要返回的那個(gè)r值r = r + 5}(r)return //空的return }

所以這個(gè)例子的結(jié)果是1。

defer確實(shí)是在return之前調(diào)用的。但表現(xiàn)形式上卻可能不像。本質(zhì)原因是return xxx語(yǔ)句并不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機(jī)會(huì)改變最終的返回值。

defer的實(shí)現(xiàn)

defer關(guān)鍵字的實(shí)現(xiàn)跟go關(guān)鍵字很類似,不同的是它調(diào)用的是runtime.deferproc而不是runtime.newproc。

在defer出現(xiàn)的地方,插入了指令call runtime.deferproc,然后在函數(shù)返回之前的地方,插入指令call runtime.deferreturn

普通的函數(shù)返回時(shí),匯編代碼類似:

add xx SP return

如果其中包含了defer語(yǔ)句,則匯編代碼是:

call runtime.deferreturn, add xx SP return

goroutine的控制結(jié)構(gòu)中,有一張表記錄defer,調(diào)用runtime.deferproc時(shí)會(huì)將需要defer的表達(dá)式記錄在表中,而在調(diào)用runtime.deferreturn的時(shí)候,則會(huì)依次從defer表中出棧并執(zhí)行

?

?

------------------------------------------------

我在補(bǔ)充一下,go 的函數(shù) 帶參數(shù)名的返回值和不帶參數(shù)名的函數(shù)返回值對(duì)defer是有影響的。

如果defer 操作的是帶參數(shù)的函數(shù)返回值的參數(shù)名,則會(huì)直接影響到函數(shù)的返回值

如果defer操作的是函數(shù)內(nèi)部的一個(gè)局部變量,這不會(huì)影響到函數(shù)的返回值

?

原因很簡(jiǎn)單,return會(huì)做兩個(gè)事情,

1、拷貝返回值到函數(shù)的返回值內(nèi)存區(qū),有如下幾個(gè)場(chǎng)景:

  • a.如果是函數(shù)的返回值帶參數(shù)名(假設(shè)參數(shù)名為a),實(shí)際在函數(shù)內(nèi)部對(duì)a的操作是直接操作這個(gè)返回值的內(nèi)存區(qū)域

如果函數(shù)返回直接調(diào)用return,不帶任何返回值, 則不會(huì)有拷貝過(guò)程。此時(shí)defer如果沒(méi)有顯示的操作a,則不會(huì)影響到函數(shù)的返回值。

如果函數(shù)返回直接調(diào)用return X,則會(huì)有個(gè)a=x的拷貝過(guò)程,此時(shí)defer如果沒(méi)有顯示的操作a,也不會(huì)影響到函數(shù)的返回值(即使操作了x也不會(huì)影響結(jié)果)。

  • b如果是函數(shù)的返回值不帶參名,函數(shù)的返回值需要一個(gè)顯示的return x語(yǔ)句,此時(shí)會(huì)有一個(gè)拷貝過(guò)程,就是將x的值拷貝到返回值的內(nèi)存區(qū)域,此時(shí)defer操作x 不會(huì)有什么副作用,此時(shí)存放返回值的內(nèi)存區(qū)域是個(gè)匿名區(qū)域,從源程序的角度看defer直接操作變量是不可能訪問(wèn)到這個(gè)區(qū)域。可見(jiàn)不帶形參的返回是避免defer副作用的最有效手段。

2、執(zhí)行RET指令,執(zhí)行跳轉(zhuǎn)

?

talk is cheep ,show me the code , 上一段代碼:

[csharp]?view plain?copy ?
  • package?main??
  • ??
  • import?(??
  • ????"fmt"??
  • )??
  • ??
  • ??
  • //f1?f2?f3?函數(shù)返回值帶有參數(shù)名??
  • func?f1()?(r?int)?{??
  • ????defer?func()?{??
  • ????????r++??
  • ????}()??
  • ????return?0??
  • }??
  • ??
  • func?f2()?(r?int)?{??
  • ????defer?func(i?int)?{??
  • ????????i++??
  • ????}(r)??
  • ????return?0??
  • }??
  • ??
  • func?f3()?(r?int)?{??
  • ????t?:=?0??
  • ????defer?func()?{??
  • ????????t++??
  • ????}()??
  • ????return?t??
  • }??
  • ??
  • ??
  • //f4?f5?函數(shù)的返回值都不帶參數(shù)名,只有類型??
  • func?f4()?int?{??
  • ????r?:=?0??
  • ????defer?func()?{??
  • ????????r++??
  • ????}()??
  • ????return?r??
  • }??
  • ??
  • func?f5()?int?{??
  • ????r?:=?0??
  • ????defer?func(i?int)?{??
  • ????????i++??
  • ????}(r)??
  • ????return?0??
  • }??
  • ??
  • func?main()?{??
  • ????var?v?int?=?-1??
  • ????v?=?f1()??
  • ??????
  • ????fmt.Println("v=",?v)??
  • ????v?=?f2()??
  • ????fmt.Println("v=",?v)??
  • ????v?=?f3()??
  • ????fmt.Println("v=",?v)??
  • ??
  • ??????
  • ????v?=?f4()??
  • ????fmt.Println("v=",?v)??
  • ????v?=?f5()??
  • ????fmt.Println("v=",?v)??
  • }??

  • 結(jié)果: [csharp]?view plain?copy ?
  • v=?1??
  • v=?0??
  • v=?0??
  • v=?0??
  • v=?0??

  • 結(jié)論:為了避免defer可能引發(fā)的歧義,在定義函數(shù)時(shí),最好使用不帶參數(shù)名的方式

    ?

    引申一下,我們來(lái)看下go函數(shù)的棧幀結(jié)構(gòu),首先學(xué)習(xí)下幾個(gè)寄存器

  • FP 棧底寄存器,指向一個(gè)函數(shù)棧的頂部;
  • PC? ?程序計(jì)數(shù)器,指向下一條執(zhí)行指令;
  • SB 指向靜態(tài)數(shù)據(jù)的基指針,全局符號(hào);
  • SP 棧頂寄存器;
  • ?

    ?

    1.go的函數(shù)調(diào)用現(xiàn)場(chǎng)清理由主調(diào)函數(shù)負(fù)責(zé)維護(hù); 2.函數(shù)的返回地址也在主調(diào)函數(shù)的棧上開(kāi)辟; 3.棧上的數(shù)據(jù)是通過(guò)FB+- 偏移量來(lái)操作; 4.return語(yǔ)句分兩部分執(zhí)行:   把返回值拷貝到返回值的棧上空間內(nèi)   調(diào)用RET指令執(zhí)行函數(shù)調(diào)用后的下一條指令 但是return的語(yǔ)句不是原子的,defer語(yǔ)句的執(zhí)行被插入在這兩條指令執(zhí)行之間,這是導(dǎo)致defer導(dǎo)致歧義的最根本原因。為避免困惑, defer 不應(yīng)該操作函數(shù)返回值存放的區(qū)域,即defer語(yǔ)句里面不應(yīng)該有對(duì)命名返回值參數(shù)的操作。 ? ?

    轉(zhuǎn)載于:https://www.cnblogs.com/ralap7/p/9194734.html

    總結(jié)

    以上是生活随笔為你收集整理的【转】golang-defer坑的本质的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。