bigywater.tech
Gin · 23

Next 的栈展开顺序与后置逻辑

“c.Next() 之后的代码”到底什么时候执行?中间件不调 Next 会怎样?响应写出以后还能改什么?panic 时哪些后半段会被跳过?这篇用打印顺序和真实客户端把洋葱模型的细节一次看清。

先进后出
三个中间件都调 Next 时,后半段的顺序与前半段相反
handler 仍执行
中间件 B 不调用 Next 时,后面的 C 与 handler 照常执行
201 ≠ 500
后半段里把状态改成 500,响应已写出时客户端仍收到 201

洋葱模型

实测 · 本机执行输出
    都调用 Next                           A前 → B前 → C前 → handler → C后 → B后 → A后

Next 是个普通的函数调用,所以后半段的顺序和前半段完全相反:A 前、B 前、C 前、handler、C 后、B 后、A 后。需要“包住”后面所有逻辑的事情(计时、日志、统一错误处理)放在 c.Next() 之后。

不调 Next 不等于停下

实测 · 本机执行输出
    B 不调用 Next                         A前 → B前 → B后 → C前 → handler → C后 → A后
    B 调用两次 Next                        A前 → B前 → C前 → handler → C后 → B后 → A后
    B 调用 Abort(不调 Next)                A前 → B前 → B后 → A后

中间件不调 c.Next() 只是不亲自递归,外层的 Next 循环会接着调下一个 handler,所以后面的中间件和 handler 仍会执行,只是执行时机变成在它的“后”半段之后。要拦截请求必须 c.Abort()(或 AbortWithStatus 系列)。调两次 Next 没有额外效果。

后半段能改什么

实测 · 本机执行输出
    中间件后半段:状态=201 已写字节=5 已写出=true Errors=1
    /x 客户端收到:状态=201 正文="hello" X-Late="" X-Early=""
    中间件后半段:状态=202 已写字节=-1 已写出=false Errors=0
    /y 客户端收到:状态=500 正文="" X-Late="1" X-Early="1"

后半段里读得到状态、已写字节数、Errors,但响应一旦写出,c.Header 和 c.Status 对客户端就无效了。handler 只设状态而没有写正文时,响应其实还没发出,后半段改状态仍然生效。写“统一加响应头”的中间件要放在 Next 之前。

panic 时

实测 · 本机执行输出
    Recovery 在最外层                      A前 → B前 → handler → [Recovery 处理 panic] → [状态500]
    Recovery 在 A 与 B 之间                A前 → B前 → handler → [Recovery 处理 panic] → A后 → [状态500]
    B 用 defer 写收尾                      A前 → handler → B的defer → [Recovery 处理 panic] → [状态500]

panic 会跳过沿途所有“后”半段,直到被 Recovery 捕获。Recovery 放在最外层,所有后半段都不会执行;放在中间,只有它外面的后半段还能执行。需要在 panic 时也做收尾(释放锁、记录指标)的中间件应该用 defer。

分步演示

洋葱模型
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。顺序用打印记录;前两节与第四节用 httptest 直接调用 ServeHTTP,第三节用真实的 httptest.NewServer 与 http.Client(因为 httptest.ResponseRecorder 不能区分头是否已发出);每种情形各一次
    • 没有验证:Recovery 在 panic 后对 c.Writer 的处理细节(见后面 Recovery 的文章);在 handler 里启动的 goroutine 里 panic 的情形(单独一篇);gin.Default() 里 Logger 在 Recovery 外层时的顺序
    京ICP备2026067502号-1京公网安备11010502063487号