前言
在第一篇 Gin - Hello World 中,我们提到过:gin.Default() 本质上就是帮你提前挂载了两个默认中间件(Logger 和 Recovery)。
这一篇我们就动手来体验一下 手动注册中间件,顺便更清楚地理解 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() 完全一致。

设置日志输出路径
既然我们可以手动注册 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() }
|
如图,可以看到日志被成功写入:

设置日志输出路径 - 方式二
除了 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() }
|
请求结果如下:

终端中也可以看到 panic:

服务虽然没有直接退出,但仍然会把 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 自动捕获 panic,并返回一个友好的错误响应(500)
控制台也输出了标准的 Recovery 日志:

实现原理解析
Recover 的实现其实很简单,本质就是 Go 的 defer + recover(),它把 panic 捕获下来,避免继续向外传播导致服务崩溃。
核心流程如下:
- 注册一个
defer
- 执行
c.Next()(执行后续中间件与 handler)
- 如果中途 panic,则 defer 捕获
- 打日志 + 给用户返回错误
下面是精简后的源码(去掉不相关逻辑):
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 都能被捕获住。