0%

从 Go 语言的 panic 中恢复

什么是 painc?

Panic 是 Go 语言中一个内置函数,它会中断正常的控制流并开始 panic 流程。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数(deferred function)都被正常执行,然后 F 返回给它的调用者。对于调用者来说,F 的行为就像对 panic 的调用。这个过程继续在堆栈中进行,直到当前 goroutine 中的所有函数都返回,这时程序就会崩溃。painc 可以通过直接调用 panic 函数来启动,也可以由运行时错误引起,如数组越界。

简单地说,painc 使一个函数不执行其预期的流程,并可能导致整个程序退出。

解决方案

Go 原生提供了一些功能,可以帮助我们从这种情况下恢复。

Defer

Go 的 defer 语句安排了一个函数:这个函数在执行 defer 的函数返回之前立即运行。

我们称 defer 调用的函数为:延迟函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Contents 将文件内容以字符串形式返回。
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close 将在函数完成后执行

var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err // 如果我们从这里返回,f 会被安全关闭
}
}
return string(result), nil // 如果我们从这里返回,f 会也会被安全关闭
}

Recover

panic 被调用时,它立即停止执行当前函数,并沿 goroutine 的堆栈运行所有延迟函数。

recover 的调用会终止 panic,并返回传递给 panic 的参数。recover 只在延迟函数中有效,因为 panic 后唯一能够运行的代码在延迟函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
}

实现

让我们来实现一个简单的数学函数,它可以将两个数字相除,如果分母是 0,就会 panic Divide by zero error!

下边的函数检查分母的值,如果它是 0,就会 panic。

1
2
3
4
5
func checkForError(y float64) { 
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}

下边这个函数负责对提供的数字进行除法操作并返回,同时它使用上面定义的函数来检查分母是否为 0。

由于 checkForError 会破坏流程,因此这个函数实现了recover()defer,以便在发生 panic 时返回 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func safeDivision(x, y float64) float64 { 
var returnValue float64
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0 }
}()
checkForError(y)

returnValue = x / y

return returnValue
}

将上边的代码组合起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
)

func main() {
fmt.Println("Pre panic execution")
value1 := safeDivision(2, 0)
fmt.Println("Post panic execution, -> ", value1)
fmt.Println("Pre valid execution")
value2 := safeDivision(2, 1)
fmt.Println("Post valid execution, value -> ", value2)
}

func safeDivision(x, y float64) float64 {
var returnValue float64
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0
}
}()
checkForError(y)

returnValue = x / y

return returnValue
}

func checkForError(y float64) {
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}

输出为:

1
2
3
4
5
6
Pre panic execution
Panic occured: Divident cannot be 0! Divide by 0 error.
Returning safe values
Post panic execution, -> 0
Pre valid execution
Post valid execution, value -> 2