错误恢复

目标

  • 实现错误处理机制。

panic、defer、recover

panic

介绍:

panic 是一个 Go 内置函数,它用来停止当前常规控制流并启动 panicking(运行时恐慌)过程。当函数 F 调用 panic 函数时,函数 F 的执行停止,函数 F 中已进行了求值的 defer 函数都将得到正常执行,然后函数 F 将控制权返还给其调用者。函数 F 之后的行为就如同调用者调用的函数是 panic 一样,该 panicking(运行时恐慌)过程将继续在栈上进行下去,直到当前 goroutine 中的所有函数都返回为止,此时程序将崩溃退出。

Go 语言中,比较常见的错误处理方法是返回 error,由调用者决定后续如何处理。但是如果是无法恢复的错误,可以手动触发 panic,当然如果在程序运行过程中出现了类似于数组越界的错误,panic 也会被触发。panic 会中止当前执行的程序,退出。

主动触发panic的例子:

1
2
3
4
5
func main() {
fmt.Println("before panic")
panic("crash")
fmt.Println("after panic")
}
1
2
3
4
5
func main() {
arr := []int{1, 2, 3}
fmt.Println(arr[4])
}
// panic: runtime error: index out of range [4] with length 3

defer

panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经defer 的任务,执行完成后再退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
defer func() {
fmt.Println("defer func")
}()

arr := []int{1, 2, 3}
fmt.Println(arr[4])
}

// 输出
/*
defer func
panic: runtime error: index out of range [4] with length 3
*/

recover

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func test_recover() {
defer func() {
fmt.Println("defer func")
if err := recover(); err != nil {
fmt.Println("recover success")
}
}()

arr := []int{1, 2, 3}
fmt.Println(arr[4])
fmt.Println("after panic")
}

func main() {
test_recover()
fmt.Println("after recover")
}

// 输出
/*
defer func
recover success
after recover
*/

recover 捕获了 panic,程序正常结束。test_recover() 中的 after panic 没有打印,这是正确的,当 panic 被触发时,控制权就被交给了 defer 。在 main() 中打印了 after recover,说明程序已经恢复正常,继续往下执行直到结束。

设计错误处理机制

对一个 Web 框架而言,错误处理机制是非常必要的。如果框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。我们要避免因为这些原因导致系统宕机。

添加一个非常简单的错误处理机制,即在此类错误发生时,向用户返回 Internal Server Error,并且在日志中打印必要的错误信息,方便进行错误定位。

将错误处理设置成中间件

recovery.go

Recovery 的实现:

  1. 使用 defer 挂载上错误恢复的函数。
  2. 在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error
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
37
38
39
40
package gee

import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)

// 获取触发 panic 的堆栈信息
func trace(message string) string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:]) // skip first 3 caller

var str strings.Builder
str.WriteString(message + "\nTraceback:")
for _, pc := range pcs[:n] {
fn := runtime.FuncForPC(pc)
file, line := fn.FileLine(pc)
str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
}
return str.String()
}

func Recovery() HandlerFunc {
return func(c *Context) {
// 使用 defer 挂载上错误恢复的函数。
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
// 捕获 panic,并且将堆栈信息打印在日志中
log.Printf("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()

c.Next()
}
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"net/http"

"gee"
)

func main() {
r := gee.Default()
r.GET("/", func(c *gee.Context) {
c.String(http.StatusOK, "Hello jpc\n")
})
// 越界触发panic测试
r.GET("/panic", func(c *gee.Context) {
names := []string{"jpc"}
c.String(http.StatusOK, names[100])
})

r.Run(":9999")
}

运行后访问,发现程序没有因为错误而宕机,而是返回”Internal Server Error”。

image-20221213203513579

项目地址

项目地址