2023年11月29日发(作者:)

理解DeferPanicRecover

刚开始的时候理解如何使⽤DeferRecover有⼀点怪异,尤其是使⽤了try/catch块的时候。有⼀种模式可以在Go中实现和try/catch语句块⼀

样的效果。不过之前你需要先领会DeferPanicRecover的精髓。

⾸先你需要理解defer关键字的作⽤,请看如下的代码:

package main

import (

"fmt"

)

func main() {

test()

}

func minicError(key string) error {

return ("mimic error: %s", key)

}

func test() {

n("start test")

err := minicError("1")

defer func() {

n("start defer")

if err != nil {

n("defer error:", err)

}

}()

n("end test")

}

mimicError⽅法是⼀个⽤来模拟错误的测试⽅法。这个⽅法按照Go语⾔的习惯返回错误。

Go中错误类型被定义为⼀个借⼝:

type error interface {

Error() string

}

如果你现在还理解不了Go的接⼝,下⾯的内容会有所帮助。任何实现了Error()⽅法的类型的变量都可以作为error类型的变量使⽤。

MimicError⽅法使⽤(string)⽅法创建了⼀个error类型的变量。errors类型可以在errors包种找到。

测试⽅法会有如下的输出:

start test

end test

start defer

defer error: mimic error:

仔细观察测试⽅法的输出你会发现这个⽅法是什么时候开始,什么时候结束的。在测试⽅法正常结束前,函数内部的defer⽅法被调⽤。两个

有趣的事情会发⽣:⾸先,defer关键字修饰的⽅法会在测试⽅法结束后被调⽤。其次,由于Go⽀持使⽤闭包,err变量可以被内部函数访

问,他的错误值“mimic error1”输出到了stdout

你可以任意时候在你的函数内部定义⼀个defer⽅法。如果那个defer⽅法需要⽤到状态,⽐如上⾯的代码中的err变量,那么这个变量必须在

defer⽅法定义之前就已经存在。

下⾯对测试⽅法稍作修改:

start test

end test

start defer

defer error: mimic error: 2

这个输出和之前的输出⼏乎没有区别,只修改了⼀点。这⼀次的defer⽅法的输出是“mimic error: 2”。很明显,defer⽅法对err变量有⼀个引

⽤。所以如果err变量的状态在defer⽅法调⽤前改变了,你就会看到修改之后的值。再次修改defer⽅法对err变量的引⽤。这次在测试⽅法和

defer⽅法中使⽤err变量的内存地址。

从下⾯的输出中你会发现,defer⽅法拥有和测试⽅法⼀样的err变量地址。

start test

err address: 0x20818a250

end test

start defer

err address in defer: 0x20818a250

defer error: mimic error: 2

只要defer⽅法放在测试⽅法结束前,那么defer⽅法就⼀定会被执⾏。这很好,但是我想要的是每次测试⽅法在执⾏的时候defer⽅法就⾸先

执⾏。这样就只能把这个⽅法放在调⽤⽅法的最前⾯。就如Occam所说:如果你有两个竞争的理论有完全⼀样的预期,那么更简单的那个

就是更好。我需要的就是⼀个简单的不需要思考的可⾏的模式(pattern)。

唯⼀的问题是err变量需要定义在defer语句的前⾯。幸运的是Go允许返回的变量直接⽤于赋值。请看下⾯修改后的代码:

package main

import (

"fmt"

)

func main() {

if err := test(); err != nil {

("mimic error: %vn", err)

}

}

func mimicError(key string) error {

return ("mimic error: %s", key)

}

func test() (err error) {

defer func() {

n("start defer")

if err != nil {

n("defer error:", err)

}

}()

n("start test")

err = mimicError("1")

n("end test")

return err

}

测试⽅法定义了⼀个返回类型为error的变量。这样err变量⽴刻就存在了,并且你可以在defer语句中访问到。同时,test⽅法也遵循了Go

⾔的惯例--给调⽤者返回⼀个错误类型。

运⾏这段代码你会得到这样的输出:

start test

end test

start defer

defer error: mimic error: 1

mimic error: mimic error: 1

现在就是时候讨论⼀下panic了。当Go的任何⽅法调⽤了panic的时候,程序的正常执⾏流程停⽌。调⽤panic的⽅法⽴刻停⽌并触发⽅

法调⽤栈的panic链。所有在同⼀个调⽤栈的⽅法都会⼀个接⼀个的停⽌,就像多⽶诺⾻牌⼀样。最终panic链会执⾏到栈顶,然后程序崩

溃。⼀个好的地⽅是全部存在的defer⽅法都会在panic序列中执⾏,并且他们可以停⽌崩溃。

下⾯的测试⽅法调⽤了内置的panic⽅法,并且从这⼀调⽤中恢复:

仔细看⼀下defer⽅法:

defer func() {

n("start panic defer")

if r := recover(); r != nil {

n("defer panic:", r)

}

}()

defer⽅法调⽤了另⼀个内置的⽅法叫做recover。这个recover⽅法阻⽌了panic触发的奔溃链继续向上调⽤。recover⽅法只可以在defer

⽅法中调⽤,这是因为panic链的⽅法中只有defer⽅法可以被执⾏。

如果recover⽅法被调⽤,但是没有任何的panic发⽣,recover⽅法只会返回nil。如果有panic发⽣,那么panic就停⽌并且给panic的赋

值会被返回。上次的代码没有调⽤MimicError⽅法,⽽是⽤内置的panic⽅法模拟了⼀个panic。运⾏代码后产⽣的输出:

start test

start defer

defer panic: Mimic Panic

defer⽅法可以捕获panic,把它打印在屏幕上并停⽌panic链的继续执⾏。同时需要注意的是“End Test”没有显⽰在屏幕上。测试⽅法在panic

调⽤的时候就⽴刻停⽌了。

看起来不错,但是还有⼀个问题:我还是想显⽰“End Test”defer很酷的地⽅在于你可以在⽅法⾥放多余⼀个的defer⽅法。

上⾯的⽅法可以修改如下:

start test

start defer

defer error: mimic error: 1

start panic defer

defer panic: Mimic Panic

mimic error: mimic error: 1

现在两个defer⽅法都放在了测试⽅法的开始部分。第⼀个deferpanicrecover,之后打印错误。⼀个需要注意的地⽅Go语⾔会按照

defer⽅法定义的反⽅向执⾏(先进先出)。

运⾏之后的输出:

start test

start defer

defer error: mimic error: 1

start panic defer

defer panic: Mimic Panic

mimic error: mimic error: 1

测试⽅法按照预期的调⽤了panic停⽌了测试⽅法本⾝的执⾏。之后处理错误的defer⽅法被⾸先执⾏。由于测试⽅法在panic之前调⽤了

mimicError⽅法,所以error可以打印出来。之后recover⽅法被调⽤,panic链被中断。

这段代码还是有⼀个问题。main⽅法根本不知道panic已经被处理了。main⽅法只知道发⽣了⼀个错误。就是mimicError⽅法模拟的错

误。这可不⾏。我需要main⽅法知道引发了panic的错误。这个更是需要报出来的错误。

我们需要在处理panicdefer⽅法中把panic的错误信息赋值给err变量。现在的输出:

start test

start defer

defer error: mimic error: 1

start panic defer

defer panic: Mimic Panic

mimic error: Mimic Panic

这个时候main函数可以打印出引起panic的错误了。

虽然看起来已经很完美了,但是这个代码不容易扩展。有两个内置的defer⽅法很酷但是不实⽤。我需要的是⼀个单个的,既可以除了错

误⼜可以处理panic的⽅法。这⾥是提炼过后的全部代码,叫做_CatchPanic

package main

import (

"fmt"

)

func main() {

if err := test(); err != nil {

("Main error: %vn", err)

}

}

func catchPanic(err error, functionName string) {

if r := recover(); r != nil {

("%s: PANIC Defered: %vn", functionName, r)

if err != nil {

err = ("%v", r)

}

}else if err != nil {

("%s: ERROR: %vn", functionName, err)

}

}

func mimicError(key string) error {

return ("Mimic Error: %s", key)

}

func test() (err error) {

defer catchPanic(err, "Test")

n("Start Test")

err = mimicError("1")

n("End Test")

return err

}

新⽅法catchPanic把错误和panic都处理了。这⾥主要实⽤了外部定义defer⽅法体的⽅式代替了内部定义⽅法体。在开始测试以前,我

们需要确定不会破坏已有的错误处理。运⾏代码后的输出:

Start Test

End Test

Main error: Mimic Error: 1

现在我们测试⼀下panic

func test() (err error) {

defer catchPanic(err, "Test")

n("Start Test")

err = mimicError("1")

panic("Mimic Panic")

// n("End Test")

return err

}

输出结果

Start Test

Test: PANIC Defered: Mimic Panic

Main error: Mimic Error: 1

好吧,我们⼜有⼀个问题。main⽅法打印了err变量的信息,⽽不是panic的内容。那是什么东西出错了呢?

func catchPanic(err error, functionName string) {

if r := recover(); r != nil {

("%s: PANIC Defered: %vn", functionName, r)

if err != nil {

err = ("%v", r)

}

}else if err != nil {

("%s: ERROR: %vn", functionName, err)

}

}

因为defer调⽤的是外部定义的⽅法。所以没有了inline⽅法或者闭包的好处。修改代码,打印出测试⽅法的err地址和_CatchPanic这个

defer⽅法。

func _CatchPanic(err error, functionName string) {

if r := recover(); r != nil {

("%s: PANIC Defered: %vn", functionName, r)

n("Err addr defer:", &err)

if err != nil {

err = ("%v", r)

}

}else if err != nil {

("%s: ERROR: %vn", functionName, err)

if err := testFinal(); err != nil {

("Main error: %vn", err)

}

}

func _CatchPanic(err *error, functionName string) {

if r := recover(); r != nil {

("%s: PANIC Defered: %vn", functionName, r)

n("Err addr defer:", &err)

if err != nil {

*err = ("%v", r)

}

}else if err != nil && *err != nil {

("%s: ERROR: %vn", functionName, *err)

}

}

func testFinal() (err error) {

defer _CatchPanic(&err, "TestFinal")

("Start Testn")

err = minicError("1")