目 录CONTENT

文章目录
Go

06.跟着Geek兔兔学习- 错误恢复(Panic Recover)

Hello!你好!我是村望~!
2023-09-07 / 0 评论 / 0 点赞 / 134 阅读 / 1,486 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

错误恢复(Panic Recover)

感谢Geek 兔的教程

示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解

panic

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

// hello.go
func main() {
	fmt.Println("before panic")
	panic("crash")
	fmt.Println("after panic")
}
$ go run hello.go

before panic
panic: crash

goroutine 1 [running]:
main.main()
        ~/go_demo/hello/hello.go:7 +0x95
exit status 2

下面是数组越界触发的 panic

// hello.go
func main() {
	arr := []int{1, 2, 3}
	fmt.Println(arr[4])
}
$ go run hello.go
panic: runtime error: index out of range [4] with length 3

defer

panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经defer 的任务,执行完成后再退出。效果类似于 java 语言的 try...catch

// hello.go
func main() {
	defer func() {
		fmt.Println("defer func")
	}()

	arr := []int{1, 2, 3}
	fmt.Println(arr[4])
}
$ go run hello.go 
defer func
panic: runtime error: index out of range [4] with length 3

可以 defer 多个任务,在同一个函数中 defer 多个任务,会逆序执行。即先执行最后 defer 的任务。【后进先出】

在这里,defer 的任务执行完成之后,panic 还会继续被抛出,导致程序非正常结束。

recover

Go 语言还提供了 recover 函数,可以避免因为 panic 发生而导致整个程序终止,recover 函数只在 defer 中生效。

// hello.go
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")
}
$ go run hello.go 
defer func
recover success
after recover

我们可以看到,recover 捕获了 panic,程序正常结束。

test_recover() 中的 after panic 没有打印,这是正确的,当 panic 被触发时,控制权就被交给了 defer 。

就像在 java 中,try代码块中发生了异常,控制权交给了 catch

接下来执行 catch 代码块中的代码。而在 main()中打印了 after recover,说明程序已经恢复正常,继续往下执行直到结束。

Gee 的错误处理机制

对一个 Web 框架而言,错误处理机制是非常必要的。

可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。

也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。

如果因为这些原因导致系统宕机,必然是不可接受的。

如果代码中存在会触发 panic 的 BUG,很容易宕掉。

例如下面的代码:

func main() {
	r := gee.New()
	r.GET("/panic", func(c *gee.Context) {
		names := []string{"geektutu"}
		c.String(http.StatusOK, names[100]) //数组越界
	})
	r.Run(":9999")
}

在上面的代码中,我们为 gee 注册了路由 /panic,而这个路由的处理函数内部存在数组越界 names[100],如果访问 localhost:9999/panic,Web 服务就会宕掉。

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

我们之前实现了中间件机制,错误处理也可以作为一个中间件,增强 gee 框架的能力。

新增文件 gee/recovery.go,在这个文件中实现中间件 Recovery

func Recovery() HandlerFunc {
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				message := fmt.Sprintf("%s", err)
				log.Printf("%s\n\n", trace(message))
				c.Fail(http.StatusInternalServerError, "Internal Server Error")
			}
		}()

		c.Next()
	}
}

Recovery 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error

这里有一个 trace() 函数,这个函数是用来获取触发 panic 的堆栈信息,完整代码如下:

// trace函数用于生成调试时的堆栈跟踪信息
// 参数message是一个自定义的字符串消息,它将包含在输出中
func trace(message string) string {
	// 声明一个数组pcs,用于存储程序计数器(PCs)的地址
	var pcs [32]uintptr
	// 使用runtime.Callers获取调用堆栈信息,跳过前3个调用以避免包括trace函数自身
	n := runtime.Callers(3, pcs[:])

	// 创建一个字符串构建器,用于构建输出字符串
	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()
}

测试使用

我们在New中默认注册这个中间件

func NewV2() *EngineV2 {
	engine := &EngineV2{router: newTrieRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}  // 初始化RouterGroup
	engine.groups = []*RouterGroup{engine.RouterGroup} // 初始化groups切片
	engine.middlewares = append(engine.middlewares, Recovery())
	return engine
}
r := gee.NewV2()
tourister := r.Group("/tourister")
touristerV1 := tourister.Group("/v1")

touristerV1.GET("/panic", func(ctx *gee.Context) {
	names := []string{"cunwang", "laodi"}
	ctx.String(http.StatusOK, names[3]) // 超出 index
})
r.Run(":9999")

image-20230907153123624

0

评论区