mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-11-05 05:53:45 +00:00
ch5-10 done
This commit is contained in:
parent
0f739e197d
commit
a7c121074c
@ -1,3 +1,68 @@
|
||||
## 5.10. Recover捕獲異常
|
||||
|
||||
TODO
|
||||
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端。
|
||||
|
||||
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。发生panic的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
|
||||
|
||||
让我们以语言解析器的为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告错误。
|
||||
|
||||
```Go
|
||||
func Parse(input string) (s *Syntax, err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("internal error: %v", p)
|
||||
}
|
||||
}()
|
||||
// ...parser...
|
||||
}
|
||||
```
|
||||
|
||||
Parse中的deferred函数帮助Parse从panic中恢复。在deferred函数中,用panic value构造了错误信息,再错误信息赋值给err并返回给调用者,我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。
|
||||
|
||||
不加区分的恢复所有的panic异常,是值得怀疑的;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
|
||||
|
||||
虽然把对panic的处理都击中在一个包下,有助于简化对复杂和不可以预料错误的处理,但作为被广泛遵守的规范,你不应该视图去恢复其他包引起的panic。公共的API应该将函数的运行失败作为error返回。同样的,你也不应该恢复一个由其他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这些函数是安全的。
|
||||
|
||||
但事情并非如此简单,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;服务器会调用recover,输出堆栈信息,继续运行。这样的做法在实践中很便捷,但确实会引起资源泄漏,也会因为recover操作,导致其他问题。
|
||||
|
||||
基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理(在下面的例子中,我们会看到这种方式)。
|
||||
|
||||
下面的例子是title函数的变形,如果HTML页面包含多个`<title>`,该函数会给调用者返回一个错误(error)。在soleTitle内部理时,如果检测到有多个`<title>`,会调用panic,阻止函数继续递归,并将特殊类型bailout作为panic的参数。
|
||||
|
||||
```Go
|
||||
// soleTitle returns the text of the first non-empty title element
|
||||
// in doc, and an error if there was not exactly one.
|
||||
func soleTitle(doc *html.Node) (title string, err error) {
|
||||
type bailout struct{}
|
||||
defer func() {
|
||||
switch p := recover(); p {
|
||||
case nil:
|
||||
// no panic
|
||||
case bailout{}:
|
||||
// "expected" panic
|
||||
err = fmt.Errorf("multiple title elements")
|
||||
default:
|
||||
panic(p) // unexpected panic; carry on panicking
|
||||
}
|
||||
}()
|
||||
// Bail out of recursion if we find more than one nonempty title.
|
||||
forEachNode(doc, func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
|
||||
if title != "" {
|
||||
panic(bailout{}) // multiple titleelements
|
||||
}
|
||||
title = n.FirstChild.Data
|
||||
}
|
||||
}, nil)
|
||||
if title == "" {
|
||||
return "", fmt.Errorf("no title element")
|
||||
}
|
||||
return title, nil
|
||||
}
|
||||
```
|
||||
|
||||
deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时,deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同与recover没有做任何操作。(请注意:在例子中,对可预期的错误采用了panic,这违反了之前的建议,我们在此只是想向读者演示这种机制。)
|
||||
|
||||
有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。
|
||||
|
||||
**练习5.19:** 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。
|
||||
|
Loading…
Reference in New Issue
Block a user