前言
在写服务的时候,我用了一套统一的格式,所有的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()
}
初探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)
}
}
}()