一文学会 golang 的 panic 、recover 概念和实战(集成 gin 框架/打印堆栈)
一、关于 panic
panic 又称为:恐慌、宕机
panic
是 Go 语言中用于处理程序不可恢复错误的机制。当程序遇到一些严重执行错误时,就会触发 panic
。
-
- 不可恢复的错误:如数组越界、空指针解引用等
-
- 程序逻辑错误(认为编写):如执行了错误代码、启动配置文件、环境变量缺失等等…..,比如标准库中 regexp.MustCompile()
一旦触发 panic ,程序将会直接终止,直接不可用,这在生产环境当然是不允许的,我们也不需要因为某个 bug 导致全局不可用,
那么我们就可以使用官方提供的 recover 机制对 panic 进行捕获并处理
二、reocver 的使用
(一)原理
[panic触发]
│
▼
┌─────────────────────┐
│ 逆向执行当前函数内的defer栈 │
└─────────────────────┘
│
仅在defer函数内部的recover生效
(二)代码案例
func main(){
res,err:=SafeDivide(1,0)
if err!=nil{
fmt.Println(err)
}
// 其他代码
fmt.Println("hello world") // 如果 SafeDivide 进行recover,该代码才会执行,反之不执行,程序直接关闭
}
func SafeDivide(a, b int) (res int, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("panic: %v", e)
}
}()
return a / b, nil // 可能触发除零panic
}
如果没有进行 recover,主程序调用 SafeDivide 函数之后,程序之后的逻辑将不会执行,程序直接关闭
进行 recover,SafeDivide 函数即便发生 panic,也不会影响主程序后续其他代码的执行
(三)同时打印堆栈
recover 之后同时打印堆栈日志,这在生产环境排查问题将非常有用
func SafeDivide(a, b int) (res int, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("panic: %v", e)
log.Errorln("recover success.")
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
log.Errorf(" recover %s", string(buf))
}
}()
return a / b, nil // 可能触发除零panic
}
(四)gin 框架集成
如果使用的是 gin 框架,那么可以通过中间件集成到 gin 框架,对全局的 panic 进行 recover() 捕获,确保 web 程序不会出现 panic
package main
import (
"fmt"
"io"
"net/http"
"runtime"
"time"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
func main() {
r := gin.Default()
r.Use(gin.Recovery()) // 使用默认的 recover
r.Use(gin.CustomRecoveryWithWriter(io.Discard, func(c *gin.Context, r any) { // io.Discard 表示不输出到控制台,然后自定义 handler,集成到自己的 logs 日志
err, ok := r.(error)
if ok {
buf := make([]byte, 1<<16)
runtime.Stack(buf, false)
err = fmt.Errorf("[Recovery] %s panic recovered:\n%s\n%s",
time.Now(), r, buf)
err = errors.WithStack(err)
_ = c.Error(err)
} else {
err = errors.New(fmt.Sprintf("%v", r))
}
logs.Errorf("recover:%v", err) // 将 recover 日志输出到自己的 logs 里
c.AbortWithStatus(http.StatusInternalServerError)
}))
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
注意⚠️:如果开启了协程,该协程若 panic 是无法被捕获的,需要在协程内单独进行 recover()
注意事项
-
panic 会不断向上冒泡,直到主程序,中间遇到 recover 则结束,若直到主程序都没有recover,则程序直接关闭
比如 main -> A() -> B() -> C() 当函数 C() 发生 panic,会一直向上找 recover
-
panic 只能被当前的协程捕获,父协程也无法捕获
func main(){ defer func() { if e := recover(); e != nil { err = fmt.Errorf("panic: %v", e) log.Errorln("main recover success.") // 不会执行,无法捕获子协程的 panic,程序直接关闭 } }() go Divide(1,0) // 子协程发生 panic,只能被该协程自己 recover,父协程无法捕获 select{} } func Divide(a, b int) (res int, err error) { // defer func() { // if e := recover(); e != nil { // err = fmt.Errorf("panic: %v", e) // log.Errorln("Divide recover success.") // // } // }() return a / b, nil // 可能触发除零panic }
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-panic-recover-guide/
备用原文链接: https://blog.fiveyoboy.com/articles/go-panic-recover-guide/