gin恢复中间件的作用&源码解释

Gin 的恢复中间件(Recovery Middleware)的作用是确保在处理 HTTP 请求时,应用程序不会因为某个处理函数中的 panic(运行时恐慌)而崩溃。相反,中间件会捕获 panic,记录错误信息,并返回一个合适的 HTTP 响应。

以下是恢复中间件的详细作用解释:

  1. 捕获 panic

    • 中间件使用 deferrecover 机制捕获在 HTTP 请求处理过程中发生的 panic。
    • defer 确保在请求处理结束或出现 panic 时,匿名函数会被调用。
    • recover 用于捕获 panic,并防止程序崩溃。
  2. 记录错误信息

    • 当捕获到 panic 时,中间件会记录错误信息,包括错误类型、请求的详细信息(如 HTTP 头部信息)和堆栈跟踪信息。
    • 使用 logger 记录这些信息,便于后续的调试和错误分析。
  3. 处理特定错误类型

    • 中间件会检查错误类型,例如网络错误 *net.OpError,并确定是否是因为连接断开(如 “broken pipe” 或 “connection reset by peer”)。
    • 如果是这种错误,通常意味着客户端已断开连接,此时无需记录堆栈跟踪。
  4. 返回合适的 HTTP 响应

    • 对于捕获到的 panic,恢复中间件会调用一个指定的处理函数 handle,这个函数可以根据需要返回适当的 HTTP 响应(例如 500 内部服务器错误)。
    • 如果连接已经断开,则不再尝试向客户端发送响应。
  5. 继续处理剩余中间件和请求

    • 中间件在捕获 panic 并处理后,会调用 c.Next(),确保继续执行剩余的中间件和请求处理流程。

下面是恢复中间件的源码阅读分析:

package gin

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime"
	"strings"
	"time"
)

var (
	dunno     = []byte("???")
	centerDot = []byte("·")
	dot       = []byte(".")
	slash     = []byte("/")
)

// RecoveryFunc 定义可以传递给CustomRecovery的函数,类型定义:RecoveryFunc具有了func(c *Context, err any)的特性,是一种新类型
type RecoveryFunc func(c *Context, err any)


// Recovery 返回一个中间件,该中间件从任何崩溃中恢复,如果有,则写入 500。
func Recovery() HandlerFunc {
	//  是默认的io.Writer,gin用于debug(调试)错误,表示一个写入数据的对象
	//var DefaultErrorWriter io.Writer = os.Stderr
	return RecoveryWithWriter(DefaultErrorWriter)
}

// CustomRecovery 返回一个中间件,该中间件从任何panic中恢复,并调用提供的句柄func 来处理它。
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter, handle)
}

// RecoveryWithWriter 返回给定writer的中间件,该中间件从任何panic中恢复,并写入 500(如果有)。
// HandlerFunc 将 gin 中间件使用的处理程序定义为返回值。
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
	if len(recovery) > 0 {
		return CustomRecoveryWithWriter(out, recovery[0])
	}
	return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

// CustomRecoveryWithWriter 返回一个给定writer的中间件,该中间件从任何panic中恢复,并调用提供的处理func 来处理它。
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// 检查连接是否断开,因为这并不是真正需要紧急堆栈跟踪的情况。
				var brokenPipe bool
				// OpError 是 net 包中的函数通常返回的错误类型。它描述了错误的操作、网络类型和地址
				if ne, ok := err.(*net.OpError); ok {  // 将接口类型的错误 err 转换为具体的类型 *net.OpError。
					var se *os.SyscallError //SyscallError 记录来自特定系统调用的错误
					if errors.As(ne, &se) {  // As 在 err 树中查找与 target 匹配的第一个错误,如果找到,则将 target 设置为该错误值并返回 true。否则,它将返回 false
						seStr := strings.ToLower(se.Error())
						if strings.Contains(seStr, "broken pipe") ||
							strings.Contains(seStr, "connection reset by peer") {
							brokenPipe = true // 这里连接确认断开
						}
					}
				}
				if logger != nil {
					/*
					为什么是 3:

					假设 stack 函数的实现类似于 runtime.Callers 或类似的调用栈捕获函数,它需要跳过前几层来获取实际的应用代码位置。

					在这个恢复中间件中,栈帧的层次大致如下:

					1. defer 的匿名函数(在 `defer func() { ... }` 内部)
					2. `CustomRecoveryWithWriter` 返回的中间件函数
					3. 恢复中间件的调用(由 Gin 调用)
					4. 应用程序的实际处理函数(导致 panic 的地方)
					具体来说,堆栈的前三层是框架和中间件的实现细节,不太有助于调试,因此跳过它们。

					stack(3) 意味着从第 4 层开始,捕获堆栈跟踪信息,这样可以更直接地定位到用户代码中的实际问题。

					实际效果:

					通过跳过不必要的栈帧,生成的堆栈跟踪信息更加简洁,直接指向应用程序代码中的 panic 源头,便于开发者进行调试和问题定位。
					 */
					stack := stack(3) // 堆栈返回一个格式良好的堆栈帧,跳过3
					// DumpRequest 在其 HTTP 1.x 线路表示形式中返回给定的请求。它应该仅用于服务器调试客户端请求。
					// 作用是将HTTP请求的头信息转储为字符串,并隐藏Authorization头中的敏感信息
					httpRequest, _ := httputil.DumpRequest(c.Request, false)
					headers := strings.Split(string(httpRequest), "\r\n")
					for idx, header := range headers {
						current := strings.Split(header, ":")
						if current[0] == "Authorization" {
							headers[idx] = current[0] + ": *"
						}
					}

					headersToStr := strings.Join(headers, "\r\n")
					if brokenPipe {
						logger.Printf("%s\n%s%s", err, headersToStr, reset)
					} else if IsDebugging() {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
							timeFormat(time.Now()), headersToStr, err, stack, reset)
					} else {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
							timeFormat(time.Now()), err, stack, reset)
					}
				}
				if brokenPipe {
					// 如果连接已失效,我们无法向其写入状态。
					c.Error(err.(error)) //nolint: errcheck
					c.Abort() // 连接已经断开,不在做请求的后续操作
				} else {
					handle(c, err)  // 默认恢复中间件调用:defaultHandleRecovery(c *Context, _ any)
				}
			}
		}()
		c.Next()
	}
}

// 默认句柄恢复
func defaultHandleRecovery(c *Context, _ any) {
	// 中止状态为500,500:服务器内部错误,无法完成请求
	c.AbortWithStatus(http.StatusInternalServerError)
}

// stack 返回一个格式良好的堆栈帧,跳过跳过帧。
func stack(skip int) []byte {
	buf := new(bytes.Buffer) // 返回的数据
	// 当我们循环时,我们打开文件并读取它们。这些变量记录当前加载的文件。
	var lines [][]byte
	var lastFile string
	for i := skip; ; i++ { // 跳过预期的帧数
		pc, file, line, ok := runtime.Caller(i)  //  获取调用栈信息 返回值对应:程序计数器、文件名和行号,如果无法恢复信息,则布尔值 ok 为 false。
		if !ok {
			// 无法恢复信息时跳出循环
			break
		}
		// 至少打印这么多。如果我们找不到源,它就不会显示。
		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
		if file != lastFile {
			data, err := os.ReadFile(file)
			if err != nil {
				continue
			}
			lines = bytes.Split(data, []byte{'\n'})
			lastFile = file
		}
		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
	}
	return buf.Bytes()
}

// source 返回第 n 行的空格修剪切片。
func source(lines [][]byte, n int) []byte {
	n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
	if n < 0 || n >= len(lines) {
		return dunno
	}
	return bytes.TrimSpace(lines[n])
}

// function 如果可能,返回包含 PC 的函数的名称。
func function(pc uintptr) []byte {
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return dunno
	}
	name := []byte(fn.Name())
	/*
	该名称包括包的路径名,这是不必要的,因为文件名已包含在内。
	另外,它有中心点。也就是说,我们看到runtime/debug.*T·ptrmethod和想*T.ptrmethod。
	此外,包路径可能包含点(例如 code.google.com/...),因此请先去掉路径前缀
	 */
	if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
		name = name[lastSlash+1:]
	}
	if period := bytes.Index(name, dot); period >= 0 {
		name = name[period+1:]
	}
	name = bytes.ReplaceAll(name, centerDot, dot)
	return name
}

// timeFormat 返回 Logger 的自定义时间字符串。
func timeFormat(t time.Time) string {
	return t.Format("2006/01/02 - 15:04:05") // go特有的时间格式表示2006/01/02 - 15:04:05,相当于:YYYY/MM/DD - 。。。
}
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值