目录

Go 语言 panic recover 实战指南

一、关于 panic

panic 俗称 恐慌

panic 是 Go 语言中用于处理程序不可恢复错误的机制。当程序遇到无法继续执行的严重错误时,可以触发 panic

    1. 不可恢复的错误​​:如数组越界、空指针解引用等​
    1. ​​程序逻辑错误​​:如不应该发生的代码被执行、必要的配置文件缺失,比如标准库中 regexp.MustCompile() 正则表达式编写错误直接就 panic

一旦触发 panic ,程序会直接不可用

当然这不是我们想要的结果,谁也不想出现错误就导致整个程序不可用,那么就可以用 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 框架,确保 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 则结束,若直到主程序都没有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
    

    }