Go Gin的Recovery中间件是怎么实现的(如何自己写一个Recovery)

5,556 阅读3分钟

前言

在写服务的时候,我用了一套统一的格式,所有的HTTP状态码都是200,但是在panic的时候,Gin的Recovery()中间件却返回了500,这让我的返回结果变得不再统一了,因此决定自己编写一个Recovery(),顺便探究一波Gin的Recovery()是怎么实现的

一段会panic的代码

先来一段会panic的代码,这里只是在handler里面写只了一行 panic("我panic啦"),运行后访问http://localhost:8080/panic 会发现返回了500,并且控制台的输出表明了Gin Recovery()中间件捕获了这个Panic()并进行响应的处理。

package main

import "github.com/gin-gonic/gin"

func main() {
   r := gin.New()
   r.Use(gin.Logger(), gin.Recovery())
   r.GET("panic", func(c *gin.Context) {
      panic("我panic啦")
   })
   r.Run()
}

image.png

初探Gin Recovery()

Recover()源码

让我们直接进入Gin Recovery()的源码,可以发现它调用了 RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc,而RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc又调用了 CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc

可以看到CustomRecoveryWithWriter()的核心代码如下:

func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
   // 日志记录
   return func(c *Context) {
      defer func() { 
         if err := recover(); err != nil { //用于捕获panic
            // panic处理
         }
      }()
      c.Next() // 调用下一个处理
   }
}

上面的代码把日志记录和panic处理的代码去掉了,只剩下Recovery()如何捕获到panic的代码。

捕获的流程很简单,只是在调用下一个服务之前,先defer一下,然后再c.Next(),这样只要c.Next()对应的处理函数panic,就会被defer的recover()捕获到。

自定义Recovery()

知道了Gin是怎么捕获panic,那么我们也可以照着写一个Recovery():

func Recovery() func(c *gin.Context) {
   return func(c *gin.Context) {
      defer func() { 
         if err := recover(); err != nil { //用于捕获panic
            c.String(http.StatusOK, "我捕获到panic啦:" + err.(string)) <--自定义panic时的响应
         }
      }()
      c.Next() // 调用下一个处理
   }
}

func main() {
   r := gin.New()
   r.Use(gin.Logger(), Recovery()) // <--使用自定义Recovery()替代gin.Recovery()
   r.GET("panic", func(c *gin.Context) {
      panic("我panic啦")
   })
   r.Run()
}

再次访问http://localhost:8080/panic 可以看到现在返回了我们自定义的响应。

使用Gin CustomRecovery自定义Recovery

在看Recovery()源码的过程中,会发现它的下面有一个 CustomRecovery(handle RecoveryFunc) HandlerFunc方法,它和Recovery()唯一的不同就是它可以自定义怎么处理panic的handle函数,因此使用它可以复用Gin其他的Recovery逻辑,下面使用它自定义Recovery():

func Recovery() func(c *gin.Context) {
   return gin.CustomRecovery(func(c *gin.Context, err interface{}) {
      c.String(http.StatusOK, "我捕获到panic啦:" + err.(string)) <--自定义panic时的响应
   })
}

可以发现,我们不用再自己捕获panic,只要定义如何处理panic即可。

细看Gin Recovery()

前面我们把Gin Recovery()的细节都忽略了,现在我们再仔细来看。

DefaultErrorWriter

可以看到RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc的第一个参数是一个输出流,Recovery()默认会传入DefaultErrorWriter,而DefaultErrorWriter就是标准错误输出流os.Stderr,也就是panic的时候在屏幕看到的那些红色日志。

defaultHandleRecovery

在使用CustomRecovery时,我们会自定义handle处理panic,而Recovery()默认使用defaultHandleRecovery,而defaultHandleRecovery的代码很简单,如下:

func defaultHandleRecovery(c *Context, err interface{}) {
   c.AbortWithStatus(http.StatusInternalServerError)
}

也就是设置状态码为500,然后放弃继续处理,也就是我们一开始访问http://localhost:8080/panic 时看到的500响应

defer处理逻辑

下面是在defer接收到panic后的处理代码,可以看到CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc并不会马上调用handle(),而是定义了一个brokenPipe变量,该变量是用于判断网络连接是否断开

如果brokenPipe条件为假,也就是连接正常,那么直接调用handle()处理响应

而如果brokenPipe条件为真,也就是连接中断,那么只是调用c.Error(err.(error))记录错误,然后调用c.Abort()放弃继续处理请求

// func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc

defer func() {
   if err := recover(); err != nil {
      var brokenPipe bool // <--网络连接是否断开的判断条件
      if ne, ok := err.(*net.OpError); ok { // <--这里对brokenPipe进行设置
         if se, ok := ne.Err.(*os.SyscallError); ok {
            if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
               brokenPipe = true
            }
         }
      }
     
      // 日志代码忽略
     
      if brokenPipe { // <--如果连接已经中断,我们只需要简单的记录并中断处理
         c.Error(err.(error))
         c.Abort()
      } else {
         handle(c, err)
      }
   }
}()