Panic
go by example
https://gobyexample-cn.github.io/panic
panic
意味着有些出乎意料的错误发生。 通常我们用它来表示程序正常运行中不应该出现的错误, 或者我们不准备优雅处理的错误。
我们将使用 panic 来检查这个站点上预期之外的错误。 而该站点上只有一个程序:触发 panic。
panic 的一种常见用法是:当函数返回我们不知道如何处理(或不想处理)的错误值时,中止操作。 如果创建新文件时遇到意外错误该如何处理?这里有一个很好的 panic
示例。
package main
import "os"
func main() {
panic("a problem")
_, err := os.Create("/tmp/file")
if err != nil {
panic(err)
}
}
运行程序将会导致 panic: 输出一个错误消息和协程追踪信息,并以非零的状态退出程序:
$ go run panic.go
panic: a problem
goroutine 1 [running]:
main.main()
/.../panic.go:12 +0x47
...
exit status 2
注意,与某些使用 exception 处理错误的语言不同, 在 Go 中,通常会尽可能的使用返回值来标示错误。
Effective Go
panic
https://golang.org/doc/effective_go.html#panic
向调用者报告错误的通常方法是返回 error 作为一个额外的返回值。规范的 Read 方法是一个著名的实例;它返回一个字节数和一个错误。但如果错误无法恢复怎么办?有时,程序根本无法继续。
为此,有一个内置的函数panic,它实际上会产生一个运行时错误,使程序停止(但请看下一节)。这个函数接收一个任意类型的参数–通常是一个字符串–在程序死亡时打印出来。它也是一种指示不可能发生的事情的方法,比如退出一个无限循环。
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
这只是一个例子,但真正的库函数应该避免 panic。如果问题可以被 recover 或解决,让事情继续运行总比把整个程序拆掉要好。一个可能的反例是在初始化过程中:如果库真的无法自我创建,那么可以说,panic 是合理的。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
Recover
https://golang.org/doc/effective_go.html#recover
当调用 panic 时,包括隐含的运行时错误,如索引分片出界或类型断言失败,它会立即停止执行当前函数,并开始解开(unwind) goroutine 的堆栈,沿途运行任何 defer 函数。如果该解卷(unwind)到达 goroutine 的栈顶,程序就会死亡。然而,可以使用内置函数 recover 来重新获得goroutine的控制权并恢复正常执行。
对 recover 的调用会停止解卷(unwind),并返回传递给 panic 的参数。因为在解卷(unwind)时只有在defer函数内部的代码才能运行,所以 recover 只在defer函数内部有用。
recover的一个应用是在服务器内部关闭一个失败的goroutine,而不杀死其他正在执行的goroutine。
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
在这个例子中,如果do(work) panic,结果会被记录下来,goroutine会干净利落地退出,而不会打扰到其他程序。在 defer 闭包中不需要做任何其他事情,调用recover就可以完全处理这个条件。
因为除非直接从 defer 函数中调用 recover,否则 recover 总是返回nil,所以 defer 代码可以调用本身使用panic和recover的库例程而不会失败。举个例子,safeDo中的 defer 函数可能会在调用 recover 之前调用一个日志函数,而这个日志代码的运行不会受到 panic 状态的影响。
有了我们的 recovery 模式,do函数(以及它所调用的任何东西)可以通过调用 panic 来干净利落地摆脱任何糟糕的情况。我们可以用这个想法来简化复杂软件中的错误处理。让我们看看一个理想化版本的 regexp 包,它通过调用 panic 与本地错误类型来报告解析错误。下面是Error的定义、错误方法和Compile函数。
// 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
}
如果doParse panic,recover 块将把返回值设置为nil- defer 函数可以修改命名的返回值。然后,它将在对err的赋值中,通过断言它具有本地类型Error来检查问题是否是解析错误。如果没有,类型断言将失败,导致运行时错误,继续堆栈展开,就像什么都没有中断一样。这个检查意味着,如果发生了意外的事情,比如索引出界,即使我们使用panic和recover来处理解析错误,代码也会失败。
有了错误处理,错误方法(因为它是一个绑定到类型的方法,所以它的名字和内置的错误类型相同是很好的,甚至是很自然的)就可以很容易地报告解析错误,而不用担心手动解开解析栈。
if pos == 0 {
re.error("'*' illegal at start of expression")
}
虽然这个模式很有用,但它应该只在一个包内使用。Parse将其内部的 panic 调用转化为错误值;它不会将 panic 暴露给客户端。这是一个很好的规则。
顺便说一下,如果实际发生了错误,这个重新 panic 成语会改变panic值。然而,原始的和新的故障都会在崩溃报告中呈现,所以问题的根本原因仍然可见。因此,这种简单的重新panic方法通常已经足够了– 毕竟是崩溃,但如果你想只显示原始值,你可以多写一点代码来过滤意外的问题,并用原始错误重新panic。这就留给读者去练习了。