目录

一文学会 golang 的 panic 、recover 概念和实战(集成 gin 框架/打印堆栈)

一、关于 panic

panic 又称为:恐慌、宕机

panic 是 Go 语言中用于处理程序不可恢复错误的机制。当程序遇到一些严重执行错误时,就会触发 panic

    1. 不可恢复的错误​​:如数组越界、空指针解引用等​
    1. ​​程序逻辑错误​​(认为编写):如执行了错误代码、启动配置文件、环境变量缺失等等…..,比如标准库中 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/