Go 语言编程 — panic 和 recover

目录

defer,panic 和 recover

Golang 中常规的错误处理方式是直接 return error 给调用者,再由调用者决定后续的程序逻辑,或捕获、或终止、或恢复。但对于一些无法恢复的错误,返回 error 也没有意义,此时可以考虑使用 panic(惊恐)语句,表示一种自动触发的错误。除了在代码中主动触发 panic,在程序运行的过程中也可能会因为出现某些错误而触发 panic,例如:数组越界。

panic 会退出当前正在执行的程序(注意,不只是 Goroutine 协程),但是与 os.Exit(-1) 此类 “义无反顾” 的退出不同,panic 的退出更加的 “井然有序”,panic 会先递归的处理完当前 Goroutine 已经 defer 上去的任务,执行完毕后再打印调用栈,最终调用 exit(-2) 退出整个进程。

相对的,recover(恢复)关键字可以恢复 panic 造成的程序终止,并且 recover 是一个只能在 defer 函数中发挥作用的函数,在其他作用域中调用不会发挥任何作用。

可见,defer、panic、recover 三者的组合可以完成一种灵活的程序流控制。比如:在 defer 中通过 recover 截取 panic,从而达到 try/catch 的效果。

panic

panic 允许传入一个实参,通常是错误信息,panic 会打印这个字符串,以及触发 panic 的调用栈。

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    var result string = "successfully."

    var user = os.Getenv("USER_")

    go func() {
        defer func() {
            fmt.Println("defer here.")
        }()

        if user == "" {
            panic("should set user env.")
        }
    }()

    time.Sleep(1 * time.Second)
    fmt.Printf("get result %d\r\n", result)
}

结果:

$ go run main.go
defer here.
panic: should set user env.

goroutine 18 [running]:
main.main.func1(0x0, 0x0)
	/Users/mickeyfan/workspace/go/src/test/main.go:20 +0x79
created by main.main
	/Users/mickeyfan/workspace/go/src/test/main.go:14 +0x52
exit status 2

上述例子可以看出,panic 终止了程序,并且是在执行完挂载到 Goroutine 的 defer 之后退出的,同时还打印了错误堆栈。注意,当一个 Goroutine 挂载了多个 defer 的话,其执行顺序是后进先出的,即逆序执行。

另外,panic 仅能保证递归当前 Goroutine 下的所有 defer 都会被调用,但不保证其他的 Goroutine。例子:

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    defer fmt.Println("defer main") // will this be printed when panic?

    var result string = "successfully."
    var user = os.Getenv("USER_")

    go func() {
        defer fmt.Println("defer caller")
        func() {
            defer func() {
                fmt.Println("defer here.")
            }()

            if user == "" {
                panic("should set user env.")
            }
        }()
    }()

    time.Sleep(1 * time.Second)
    fmt.Printf("get result %d\r\n", result)
}

结果:

defer here.
defer caller
panic: should set user env.

goroutine 18 [running]:
main.main.func1.1(0x0, 0x0)
	/Users/mickeyfan/workspace/go/src/test/main.go:23 +0x79
main.main.func1(0x0, 0x0)
	/Users/mickeyfan/workspace/go/src/test/main.go:25 +0x9b
created by main.main
	/Users/mickeyfan/workspace/go/src/test/main.go:15 +0xcb
exit status 2

上述例子中,存在一个 main defer,main 会 sleep 1s 然后 return,在这个过程中 Goroutine 已经 panic 了,还没得当 main return,程序已经终止,所有已没有执行 main defer。而同一个 Goroutine 下挂载的 defer 都执行到了,并且是逆序执行的。

recover

recover,顾名思义是恢复被 panic 退出的程序流,是 Golang 的 handle panic(异常处理)机制。

try {
    //
} catch (Exeption e) {
    //
}

recover 只在 defer 函数中有效,如果不是在 refer 上下文中调用 recover 会直接返回 nil。

例子:

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    defer fmt.Println("defer main") // will this be printed when panic?

    var user = os.Getenv("USER_")

    go func() {
        defer fmt.Println("defer caller")
        func() {
            defer func() {
                fmt.Println("defer here.")
                if err := recover(); err != nil {
                    fmt.Println("recover success.")
                }
            }()

            if user == "" {
                panic("should set user env.")
            }
        }()
    }()

    time.Sleep(1 * time.Second)
    fmt.Println("get result")
}

结果:

$ go run main.go
defer here.
recover success.
defer caller
get result
defer main

通过 panic + recover 来简化错误处理

// Error is the type of a parse error; it satisfies the error interface.
type Error string

func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}


if pos == 0 {
    re.error("'*' illegal at start of expression")
}

在 Compile() 中,遇到问题只管 panic 就好,recover() 会捕捉 Error 类型的 panic,并将其转为 string 返回给调用者,调用 Compile 时并不会感知到 Compile 的 panic。而在其他场景中,例如:数组越界,在 recover 中做类型判断的时候仍然会 panic,并不会因为有 recover 就掩盖了。

另外,这个例子也说明了在 recover 中如果再次 panic,该 panic 还是会再次陷入,除非在 recover 中挂 defer 去再次 recover,否则程序就真的退出了。

通过这样的处理,就不再需要 err != nil 满天飞了,有问题 panic 就可以。

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/107449462