Gin - 使用中间件

前言

在第一篇 Gin - Hello World 中,我们提到过:gin.Default() 本质上就是帮你提前挂载了两个默认中间件(LoggerRecovery)。
这一篇我们就动手来体验一下 手动注册中间件,顺便更清楚地理解 Logger 和 Recovery 的实现原理。

使用中间件

Gin 的中间件机制本质上是一个典型的洋葱模型(pipeline)处理结构。每一个中间件都是一个 gin.HandlerFunc,通过 Use() 组合到路由处理链中,最终在一次请求中按顺序执行。

日志中间件

下面这个示例演示了只使用 Logger 中间件的场景。

要点说明:

  • gin.New():返回一个 不包含任何默认中间件 的 Engine。
  • r.Use(...):手动注册中间件。
  • gin.Logger():Gin 官方提供的日志中间件。

除了把 gin.Default() 改成了 gin.New()r.Use(gin.Logger()),其他代码和默认使用方式没有任何区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

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

func main() {
r := gin.New()
r.Use(gin.Logger())
r.GET("/testing", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "testing",
})
})
r.Run()
}

下面是一次普通的请求示例,可以看到效果和使用 gin.Default() 完全一致。

Gin - 使用中间件

设置日志输出路径

既然我们可以手动注册 Logger,自然就可以对它进行扩展。
例如,可以通过 gin.LoggerWithWriter 指定日志输出文件,而不是只输出到控制台。

下面的示例演示:

  • 使用 os.Create("gin.log") 创建一个日志文件
  • 获取文件的 io.Writer
  • 传给 gin.LoggerWithWriter

这样所有请求日志就会写入到项目根目录下的 gin.log 文件中(和 go.mod 同级)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"io"
"os"

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

func main() {
r := gin.New()

f, _ := os.Create("gin.log")
logFile := io.Writer(f)

r.Use(gin.LoggerWithWriter(logFile))
r.GET("/testing", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "testing",
})
})
r.Run()
}

如图,可以看到日志被成功写入:

Gin - 使用中间件

设置日志输出路径 - 方式二

除了 LoggerWithWriter,Gin 还提供了两个默认的 Writer:

  • gin.DefaultWriter(普通日志输出)
  • gin.DefaultErrorWriter(错误日志输出)

我们可以完全替换它们,让整个 Gin 的日志全部流向某个文件,而无需修改每个 Logger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"io"
"os"

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

func main() {
r := gin.New()

f, _ := os.Create("gin.log")

gin.DefaultWriter = io.MultiWriter(os.Stdout, f)
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, f)

r.Use(gin.Logger())
r.GET("/testing", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "testing",
})
})
r.Run()
}

这个方式对使用 gin.Default() 的场景特别方便,因为你不用手动替换 Logger,只需要设置 Writer 即可。

Recover 中间件

Recover 是 Gin 内置的异常捕获中间件,它的作用是防止因为某个 handler 的 panic 导致整个服务崩溃。

先来看一个没有使用 Recovery 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

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

func main() {
r := gin.New()

r.GET("/testing", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "testing",
})
panic("报错了!!")
})

r.Run()
}

请求结果如下:

Gin - 使用中间件

终端中也可以看到 panic:

Gin - 使用中间件

服务虽然没有直接退出,但仍然会把 panic 堆栈打印出来,并让请求返回 500。

添加中间件后再测试

我们加上 gin.Recovery()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

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

func main() {
r := gin.New()

r.Use(gin.Recovery())

r.GET("/testing", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "testing",
})
panic("报错了!!")
})

r.Run()
}

再次请求:

Gin - 使用中间件

可以看到:

  • 接口不再直接失败
  • Gin 自动捕获 panic,并返回一个友好的错误响应(500)

控制台也输出了标准的 Recovery 日志:

Gin - 使用中间件

实现原理解析

Recover 的实现其实很简单,本质就是 Go 的 defer + recover(),它把 panic 捕获下来,避免继续向外传播导致服务崩溃。

核心流程如下:

  1. 注册一个 defer
  2. 执行 c.Next()(执行后续中间件与 handler)
  3. 如果中途 panic,则 defer 捕获
  4. 打日志 + 给用户返回错误

下面是精简后的源码(去掉不相关逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 {
// ...省略错误判断逻辑...

if logger != nil {
const stackSkip = 3
logger.Printf("[Recovery] panic recovered:\n%s\n%s",
err, stack(stackSkip))
}

handle(c, err)
}
}()
c.Next()
}
}

这里能看出:

  • recover() 就是 Go 标准库提供的函数,不是 Gin 定义的。
  • Gin 只是对 panic 的堆栈、请求信息、输出格式做了包装。
  • 最关键的是将 c.Next() 放在 defer 后面,这样无论之后发生什么 panic 都能被捕获住。

Gin - 使用中间件
https://blog.pangcy.cn/2025/11/24/后端编程相关/go/gin/Gin - 使用中间件/
作者
子洋
发布于
2025年11月24日
许可协议