2023年11月29日发(作者:)
理解Defer、Panic和Recover
刚开始的时候理解如何使⽤Defer和Recover有⼀点怪异,尤其是使⽤了try/catch块的时候。有⼀种模式可以在Go中实现和try/catch语句块⼀
样的效果。不过之前你需要先领会Defer、Panic和Recover的精髓。
⾸先你需要理解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 error:1”输出到了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⽅法都放在了测试⽅法的开始部分。第⼀个defer从panic中recover,之后打印错误。⼀个需要注意的地⽅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的错误。这个更是需要报出来的错误。
我们需要在处理panic的defer⽅法中把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")


发布评论