Gin 的恢复中间件(Recovery Middleware)的作用是确保在处理 HTTP 请求时,应用程序不会因为某个处理函数中的 panic(运行时恐慌)而崩溃。相反,中间件会捕获 panic,记录错误信息,并返回一个合适的 HTTP 响应。
以下是恢复中间件的详细作用解释:
-
捕获 panic:
- 中间件使用
defer
和recover
机制捕获在 HTTP 请求处理过程中发生的 panic。 defer
确保在请求处理结束或出现 panic 时,匿名函数会被调用。recover
用于捕获 panic,并防止程序崩溃。
- 中间件使用
-
记录错误信息:
- 当捕获到 panic 时,中间件会记录错误信息,包括错误类型、请求的详细信息(如 HTTP 头部信息)和堆栈跟踪信息。
- 使用 logger 记录这些信息,便于后续的调试和错误分析。
-
处理特定错误类型:
- 中间件会检查错误类型,例如网络错误
*net.OpError
,并确定是否是因为连接断开(如 “broken pipe” 或 “connection reset by peer”)。 - 如果是这种错误,通常意味着客户端已断开连接,此时无需记录堆栈跟踪。
- 中间件会检查错误类型,例如网络错误
-
返回合适的 HTTP 响应:
- 对于捕获到的 panic,恢复中间件会调用一个指定的处理函数
handle
,这个函数可以根据需要返回适当的 HTTP 响应(例如 500 内部服务器错误)。 - 如果连接已经断开,则不再尝试向客户端发送响应。
- 对于捕获到的 panic,恢复中间件会调用一个指定的处理函数
-
继续处理剩余中间件和请求:
- 中间件在捕获 panic 并处理后,会调用
c.Next()
,确保继续执行剩余的中间件和请求处理流程。
- 中间件在捕获 panic 并处理后,会调用
下面是恢复中间件的源码阅读分析:
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 - 。。。
}