理解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 fmt.Errorf("mimic error: %s", key)
}

func test() {
	fmt.Println("start test")

	err := minicError("1")

	defer func() {
		fmt.Println("start defer")

		if err != nil {
			fmt.Println("defer error:", err)
		}
	}()

	fmt.Println("end test")
}

mimicError方法是一个用来模拟错误的测试方法。这个方法按照Go语言的习惯返回错误。

在Go中错误类型被定义为一个借口:

type error interface {
	Error() string
}

如果你现在还理解不了Go的接口,下面的内容会有所帮助。任何实现了Error()方法的类型的变量都可以作为error类型的变量使用。MimicError方法使用errors.New(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 {
		fmt.Printf("mimic error: %v\n", err)
	}
}

func mimicError(key string) error {
	return fmt.Errorf("mimic error: %s", key)
}

func test() (err error) {
	defer func() {
		fmt.Println("start defer")

		if err != nil {
			fmt.Println("defer error:", err)
		}
	}()

	fmt.Println("start test")

	err = mimicError("1")

	fmt.Println("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() {
		fmt.Println("start panic defer")

		if r := recover(); r != nil {
			fmt.Println("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 {
		fmt.Printf("Main error: %v\n", err)
	}
}

func catchPanic(err error, functionName string) {
	if r := recover(); r != nil {
		fmt.Printf("%s: PANIC Defered: %v\n", functionName, r)

		if err != nil {
			err = fmt.Errorf("%v", r)
		}
	}else if err != nil {
		fmt.Printf("%s: ERROR: %v\n", functionName, err)
	}
}

func mimicError(key string) error {
	return fmt.Errorf("Mimic Error: %s", key)
}

func test() (err error) {
	defer catchPanic(err, "Test")
	fmt.Println("Start Test")

	err = mimicError("1")

	fmt.Println("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")
	fmt.Println("Start Test")

	err = mimicError("1")

	panic("Mimic Panic")
	// fmt.Println("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 {
        fmt.Printf("%s: PANIC Defered: %v\n", functionName, r)

        if err != nil {
            err = fmt.Errorf("%v", r)
        }
    }else if err != nil {
        fmt.Printf("%s: ERROR: %v\n", functionName, err)
    }
}

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

func _CatchPanic(err error, functionName string) {
    if r := recover(); r != nil {
        fmt.Printf("%s: PANIC Defered: %v\n", functionName, r)

        fmt.Println("Err addr defer:", &err)

        if err != nil {
            err = fmt.Errorf("%v", r)
        }
    }else if err != nil  {
        fmt.Printf("%s: ERROR: %v\n", functionName, err)
    }
}

运行以后你会看到为什么main方法没有得到panic携带的错误:

Err addr:  0x20818c220
Start Test
Test7: PANIC Defered: Mimic Panic
Err addr defer: 0x20818c2b0
Main error: Mimic Error: 1

当测试方法给catchPanic这个defer方法传递err变量的时候,是按照传值引用的方式传递的。在Go语言中,所有的参数都是按照传值方式传递的。因此catchPanic这个defer方法有他独立的err变量的拷贝。任何的对catchPanic所拥有的err的拷贝的修改都只限于这个方法内部。

要修改由传值造成的代码的问题,就需要使用传递引用。

package main

import (
    "fmt"
)

func main() {
    if err := testFinal(); err != nil {
        fmt.Printf("Main error: %v\n", err)
    }
}

func _CatchPanic(err *error, functionName string) {
    if r := recover(); r != nil {
        fmt.Printf("%s: PANIC Defered: %v\n", functionName, r)

        fmt.Println("Err addr defer:", &err)

        if err != nil {
            *err = fmt.Errorf("%v", r)
        }
    }else if err != nil && *err != nil {
        fmt.Printf("%s: ERROR: %v\n", functionName, *err)
    }
}

func testFinal() (err error) {
    defer _CatchPanic(&err, "TestFinal")
    fmt.Printf("Start Test\n")

    err = minicError("1")

    panic("Mimic Panic")
}

func minicError(key string) error {
    return fmt.Errorf("Mimic Error: %s", key)
}

运行代码后输出:

Start Test
TestFinal: PANIC Defered: Mimic Panic
Err addr defer: 0x2081c4020
Main error: Mimic Panic

现在main方法打印出了panic的错误信息。

如果你要捕捉调用栈,需要引用“runtime”包。

使用以上方法就可以处理错误和各种panic的情况。很多情况下,这些情况只是需要记录日志或者在调用栈上处理掉。这样也会有效地降低error并保持代码整洁。然而,我学到的经验是最好只用上面的模式捕捉panic。日志什么的还是留给应用层面的逻辑处理吧。如果不这样,你可能会把错误两次写入日志。

希望这对你学习go语言有帮助!

欢迎加群互相学习,共同进步。QQ群:58099570 | 做人要厚道,转载请注明出处!

  

  

  

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-29 21:41:52

理解Defer、Panic和Recover的相关文章

GO_05_2:Golang 中 panic、recover、defer 的用法

 函数 defer 1. 它的执行方式类似其他语言中的折构函数,在函数体执行结束后按照调用顺序的 相反顺序 逐个执行 2. 即使函数发生 严重错误 也会被执行,类似于 java 中 try{...} catch(){} finally{} 结构的 finally 3. 支持匿名函数的调用 4. 常用于资源清理.文件关闭.解锁以及记录时间等善后操作 5. 通过与匿名函数配合可在 return 之后修改函数计算结果 6. 如果函数体内某个变量作为 defer 时匿名函数的参数,则在定义 defer

Go基础系列:defer、panic和recover

defer关键字 defer关键字可以让函数或语句延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束.即便已经panic().即便函数已经return了,也都会执行defer所推迟的对象. 例如: func main() { a() } func a() { println("in a") defer b() println("leaving a") //到了这里才会执行b() } func b() { println("in b&qu

go语言中使用defer、panic、recover处理异常

go语言中的异常处理,没有try...catch等,而是使用defer.panic.recover来处理异常. 1.首先,panic 是用来表示非常严重的不可恢复的错误的.在Go语言中这是一个内置函数,如果在程序中遇到异常,或者调用panic函数,程序会立即退出(除非recover).如下代码: package main import "fmt" func main() { a := 10 b := 0 c := a / b fmt.Println(c) } 程序的输出如下: ? de

defer, panic, recover使用总结

1. defer : 延迟调用.多个defer,依次入栈,在函数即将退出时,依次出栈调用 1 package main 2 import "fmt" 3 func main() { 4 defer func() { 5 fmt.Println("defer one") 6 }() 7 defer func() { 8 fmt.Println("defer two") 9 }() 10 defer func() { 11 fmt.Println(&

Go语言圣经-Panic异常,Recover捕获异常习题

Go语言圣经-Panic异常1.当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)2.不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常:panic函数接受任何值作为参数.3.由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致,对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic4.为了方便诊断问题,runtime包允许输出堆栈信息 Go语言圣经-Reco

golang错误处理机制:panic与recover

原文地址:http://www.niu12.com/article/14 panic知识点 package main import ( "fmt" "github.com/pkg/errors" ) func main() { outerFunc() fmt.Println(1) } func outerFunc() { innerFunc() } func innerFunc() { panic(errors.New("An intended fatal

error、panic、recover、panicking

错误处理:当程序处于错误状态可以用os.Exit(1)来中止运行自定义错误:err := errors.New("I am error")用fmt创建错误(和print一个样,它会创建一个自定义error,字符串就是格式化后的字符串)fmt.Errorf("math: square root of negative number %g", f) 运行时异常与panic 当发生运行时错误时,Go会触发运行时panic(例如数组下标越界) panic也可以从代码中初始化

defer、panic和recover

1. defer(1)defer用于将某个方法或语句推迟到当前函数返回的最后一刻执行,一般用于释放某些已分配的资源.函数返回的最后一刻指的是,return语句更新返回值变量之后,函数返回之前,所以defer语句甚至可用于修改函数的返回值(函数头部命名了返回值的情况).(2)若defer语句中嵌套了多层函数调用,只是最后一层函数调用才延后执行,其他都按代码执行顺序执行,例如:defer un(trace("b")),先按代码执行顺序执行trace("b"),假设返回值

golang panic and recover

panic 是一个内置函数,当一个函数 F 调用 panic,F 的执行就会停止,F 中 deferred 函数调用会被执行,然后 F 返回控制到它的调用者.这个过程会沿着调用栈执行下去,直到当前 goroutine 中的所有函数返回,然后程序 crash.出现 panic 是因为: 调用了 panic 函数 出现了运行时错误(例如,数组越界访问) recover 是一个内置函数,用于恢复一个 panicking goroutine 的控制.需要注意的是,recover 只能使用在 deferr