go defer-recover-panic 學(xué)習(xí)

本文將會(huì)講解defer, recover,panic相關(guān)的知識(shí)。主要內(nèi)容包括:

  • defer的原理
  • panic與recover的原理及注意事項(xiàng)

其中重點(diǎn)在defer的原理。這部分包含了defer的定義、規(guī)則、實(shí)現(xiàn)原理、內(nèi)部函數(shù)順序四部分。

希望看完本文,你能對defer、recover、panic有個(gè)全面的認(rèn)識(shí)~

一、defer的原理

定義

1、defer語句用于延遲函數(shù)的調(diào)用,每次defer都會(huì)把一個(gè)函數(shù)壓入棧中,主函數(shù)(創(chuàng)建defer的函數(shù))返回前再把延遲的函數(shù)取出并執(zhí)行。defer最常見的場景是完成一些收尾的工作,比如文件句柄的關(guān)閉等。還有就是執(zhí)行 recover, 實(shí)現(xiàn)類似其他語言中的try catch finally。
2、延遲函數(shù)可能有輸入?yún)?shù),這些參數(shù)可能來源于定義defer的函數(shù),延遲函數(shù)也可能引用主函數(shù)用于返回的變量,也就是說延遲函數(shù)可能會(huì)影響主函數(shù)的一些行為。

規(guī)則

規(guī)則一、其實(shí)使用defer時(shí),用一個(gè)簡單的轉(zhuǎn)換規(guī)則改寫一下,就不會(huì)迷糊了。改寫規(guī)則是將return語句拆成兩句寫,return xxx會(huì)被改寫成:
返回值 = xxx
調(diào)用defer的函數(shù)
空的return
規(guī)則一的幾個(gè)案例

下面通過一些案例進(jìn)行總結(jié):

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

延遲函數(shù)fmt.Println(aInt) 在defer語句出現(xiàn)時(shí)就已經(jīng)確定了,所以無論后面如何修改aInt的值都不會(huì)影響延遲函數(shù)。
上述程序轉(zhuǎn)換之后是這樣的:

func deferFuncParameter() {    
    var aInt = 1
    anonymous = aInt // anonymous為匿名的變量
    aInt = 2
    fmt.Println(anonymous)
    return
}

即使是結(jié)構(gòu)體,也是傳值,也不會(huì)影響。比如

type Test struct {
    value int
}

func (t Test) print() {
    println(t.value)
}

func main() {
    test := Test{}
    defer test.print()
    test.value += 1
}

這段代碼輸出的也是0.
如果是結(jié)構(gòu)體指針,則會(huì)影響輸出。

type Test struct {
    value int
}

func (t *Test) print() {
    println(t.value)
}

func main() {
    test := Test{}
    defer test.print()
    test.value += 1
}

這個(gè)輸出的就是1, 因?yàn)閭鬟f的指針。

題目二
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()
}

函數(shù)deferFuncParameter定義了一個(gè)數(shù)組,通過defer調(diào)用printArray, 最后修改數(shù)組的第一個(gè)元素。printArray 函數(shù)接收數(shù)組的指針,即數(shù)組的地址,由于延遲函數(shù)執(zhí)行時(shí)機(jī)在return語句之前,所以對數(shù)組的最終修改值被打印出來。

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

函數(shù)的return語句并不是原子的,實(shí)際執(zhí)行分為設(shè)置返回值->ret。defer語句實(shí)際執(zhí)行在主函數(shù)返回(ret)前,即擁有defer的函數(shù)返回過程是 : 設(shè)置返回值->執(zhí)行defer->ret。所以return語句先把result設(shè)置為i的值,即1,defer語句中又把result遞增1,所以最終返回的是2.
上述程序可以轉(zhuǎn)換為

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

總結(jié)上文的例子可以得出如下幾個(gè)結(jié)論:

  • 延遲函數(shù)的參數(shù)在defer語句出現(xiàn)時(shí)就已經(jīng)確定下來了
    如果是字面量,則肯定不受影響(如題目一); 如果是指針類型,規(guī)則仍然適用,只不過延遲函數(shù)的參數(shù)是一個(gè)地址值,這種情況下defer后面的語句對變量的修改可能會(huì)影響延遲函數(shù)(如題目二)。
  • 延遲函數(shù)可能操作主函數(shù)的具名返回值
    關(guān)鍵字return不是一個(gè)原子操作,實(shí)際上return只代理匯編指令ret,即將跳轉(zhuǎn)程序執(zhí)行。比如語句return i,實(shí)際上分兩步進(jìn)行,即將i值存入棧中作為返回值,然后執(zhí)行跳轉(zhuǎn),而defer的執(zhí)行時(shí)機(jī)正是跳轉(zhuǎn)前,所以說defer執(zhí)行時(shí)還是有機(jī)會(huì)操作返回值的。
規(guī)則二、從主函數(shù)返回值的角度看,有如下的幾條規(guī)則:
1、主函數(shù)擁有匿名返回值,返回字面值
func foo() int {    
    var i int
    defer func() {
        i++
    }()    
    return 1
}

一個(gè)主函數(shù)擁有一個(gè)匿名的返回值,返回時(shí)使用字面值,比如”1“, ”hello“這樣的值,這種情況下defer是無法操作返回值的。

2、主函數(shù)擁有匿名返回值,返回變量

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

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

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

anony = i
i++
return

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

3、主函數(shù)擁有具名返回值

主函數(shù)聲明語句中帶有名字的返回值,會(huì)被初始化一個(gè)局部變量,函數(shù)內(nèi)部可以像使用局部變量一樣使用該返回值。如果defer語句操作該返回值,可能會(huì)改變返回結(jié)果。

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

上面的函數(shù)拆解之后是這樣的:

ret = 0
ret++
return

defer實(shí)現(xiàn)原理

數(shù)據(jù)結(jié)構(gòu)

每個(gè)goroutine數(shù)據(jù)結(jié)構(gòu)中實(shí)際上也有一個(gè)defer指針,該指針指向一個(gè)defer的單鏈表,每次聲明一個(gè)defer時(shí)就將defer插入到單鏈表表頭,每次執(zhí)行defer時(shí)就從單鏈表表頭取出一個(gè)defer執(zhí)行。


image.png
defer的創(chuàng)建和執(zhí)行

源碼包src/runtime/panic.go定義了兩個(gè)方法分別用于創(chuàng)建defer和執(zhí)行defer。

  • deferproc(): 在聲明defer處調(diào)用,其將defer函數(shù)存入goroutine的鏈表中;
  • deferreturn():在return指令,準(zhǔn)確的講是在ret指令前調(diào)用,其將defer從goroutine鏈表中取出并執(zhí)行。

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

defer內(nèi)部函數(shù)順序

func TestDefer(t *testing.T) {
    fmt.Println("a")
    defer fmt.Println("b")
    defer c()
    defer d()
    fmt.Println("f")
}

func c() {
    fmt.Println("c")
}

func d() func(){
    fmt.Println("d")
    return func() {
        fmt.Println("e")
    }
}

輸出為 a f d c b
結(jié)論:defer函數(shù)在函數(shù)執(zhí)行結(jié)束后執(zhí)行,若有多個(gè)defer函數(shù),則執(zhí)行順序?yàn)楹筮M(jìn)先出。 主函數(shù)中,defer d() 并沒有執(zhí)行d()返回的閉包,所以結(jié)果里面并沒有返回e.

func TestDefer(t *testing.T) {
    fmt.Println("a")
    defer fmt.Println("b")
    defer c()
    defer d()()
    fmt.Println("f")
}

func c() {
    fmt.Println("c")
}

func d() func(){
    fmt.Println("d")
    return func() {
        fmt.Println("e")
    }
}

這段代碼只是在調(diào)用d方法時(shí)加了個(gè)括號(hào),那么d方法返回的方法就會(huì)立即執(zhí)行
返回結(jié)果為 a d f e c b 。 為什么不是 a f d e c b 呢? 這是因?yàn)樵赿efer d()() 編譯時(shí),首先定義了函數(shù)d(), 此時(shí)就輸出了d. 然后返回包含e的閉包函數(shù)。
即被defer標(biāo)記的d函數(shù)中的程序“立即執(zhí)行”,而d函數(shù)返回的函數(shù)則在測試方法結(jié)束后 按照“后進(jìn)先出”的順序執(zhí)行。

再看一個(gè) 來自effective go的例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

結(jié)果會(huì)打印

entering: b
in b
entering: a
in a
leaving: a
leaving: b

沒執(zhí)行以前我以為是如下的結(jié)果:

in b
in a
entring: a
leaving: a
entring: b
leaving: b

為什么不對呢?
因?yàn)樵诰幾g時(shí),b() 中的 defer un(trace("b")) ,是對un()函數(shù)的延遲,但是此時(shí)會(huì)執(zhí)行trace("b")。
這個(gè)例子說明,defer標(biāo)記的函數(shù)只是最外層的函數(shù),如果defer標(biāo)記函數(shù)的參數(shù)也是個(gè)函數(shù),則作為參數(shù)的函數(shù)在編譯時(shí)就會(huì)被執(zhí)行了,不必等到defer標(biāo)記函數(shù)執(zhí)行時(shí)才執(zhí)行。

二、panic與recover的原理及注意事項(xiàng)

  • panic內(nèi)置函數(shù)停止當(dāng)前goroutine的正常執(zhí)行,當(dāng)函數(shù)F調(diào)用panic時(shí),函數(shù)F的正常執(zhí)行被立即停止,然后運(yùn)行所有在F函數(shù)中的defer函數(shù),然后F返回到調(diào)用他的函數(shù)對于調(diào)用者G,F(xiàn)函數(shù)的行為就像panic一樣,終止G的執(zhí)行并運(yùn)行G中所defer函數(shù),此過程會(huì)一直繼續(xù)執(zhí)行到goroutine所有的函數(shù)。panic可以通過內(nèi)置的recover來捕獲。
  • recover內(nèi)置函數(shù)用來管理含有panic行為的goroutine,recover運(yùn)行在defer函數(shù)中,獲取panic拋出的錯(cuò)誤值,并將程序恢復(fù)成正常執(zhí)行的狀態(tài)。如果在defer函數(shù)之外調(diào)用recover,那么recover不會(huì)停止并且捕獲panic錯(cuò)誤如果goroutine中沒有panic或者捕獲的panic的值為nil,recover的返回值也是nil。由此可見,recover的返回值表示當(dāng)前goroutine是否有panic行為

幾個(gè)注意的問題

1、defer 表達(dá)式的函數(shù)如果定義在 panic 后面,該函數(shù)在 panic 后就無法被執(zhí)行到
func main() {
    panic("a")
    defer func() {
        fmt.Println("b")
    }()
}

結(jié)果 b沒有打印出來
而在defer后panic

func main() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

結(jié)果b被正常打印。

2、F中出現(xiàn)panic時(shí),F(xiàn)函數(shù)會(huì)立刻終止,不會(huì)執(zhí)行F函數(shù)內(nèi)panic后面的內(nèi)容,但不會(huì)立刻return,而是調(diào)用F的defer,如果F的defer中有recover捕獲,則F在執(zhí)行完defer后正常返回,調(diào)用函數(shù)F的函數(shù)G繼續(xù)正常執(zhí)行
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("繼續(xù)執(zhí)行")
}

func F() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", err)
        }
        fmt.Println("b")
    }()
    panic("a")
}

結(jié)果

捕獲異常: a
b
繼續(xù)執(zhí)行
c
3、如果F的defer中無recover捕獲,則將panic拋到G中,G函數(shù)會(huì)立刻終止,不會(huì)執(zhí)行G函數(shù)內(nèi)后面的內(nèi)容,但不會(huì)立刻return,而調(diào)用G的defer...以此類推
func G() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", err)
        }
        fmt.Println("c")
    }()
    F()
    fmt.Println("繼續(xù)執(zhí)行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

結(jié)果

b
捕獲異常: a
c
4、如果一直沒有recover,拋出的panic到當(dāng)前goroutine最上層函數(shù)時(shí),程序直接異常終止
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("繼續(xù)執(zhí)行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

結(jié)果

b
c
panic: a

goroutine 1 [running]:
main.F()
    /xxxxx/src/xxx.go:61 +0x55
main.G()
    /xxxxx/src/xxx.go:53 +0x42
exit status 2
5、recover都是在當(dāng)前的goroutine里進(jìn)行捕獲的,這就是說,對于創(chuàng)建goroutine的外層函數(shù),如果goroutine內(nèi)部發(fā)生panic并且內(nèi)部沒有用recover,外層函數(shù)是無法用recover來捕獲的,這樣會(huì)造成程序崩潰
func G() {
    defer func() {
        //goroutine外進(jìn)行recover
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", err)
        }
        fmt.Println("c")
    }()
    //創(chuàng)建goroutine調(diào)用F函數(shù)
    go F()
    time.Sleep(time.Second)
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    //goroutine內(nèi)部拋出panic
    panic("a")
}

結(jié)果:

b
panic: a

goroutine 5 [running]:
main.F()
    /xxxxx/src/xxx.go:67 +0x55
created by main.main
    /xxxxx/src/xxx.go:58 +0x51
exit status 2
6、recover返回的是interface{}類型而不是go中的 error 類型,如果外層函數(shù)需要調(diào)用err.Error(),會(huì)編譯錯(cuò)誤,也可能會(huì)在執(zhí)行時(shí)panic
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", err.Error())
        }
    }()
    panic("a")
}

編譯錯(cuò)誤,結(jié)果

err.Error undefined (type interface {} is interface with no methods)
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", fmt.Errorf("%v", err).Error())
        }
    }()
    panic("a")
}

結(jié)果:

捕獲異常: a

參考文獻(xiàn)

Go defer實(shí)現(xiàn)原理剖析
理解 Go 語言 defer 關(guān)鍵字的原理
defer關(guān)鍵字
golang中的defer函數(shù)的執(zhí)行順序
go defer,panic,recover詳解 go 的異常處理
effective_go中文版
談?wù)?panic 和 recover 的原理,講的比較深入

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容