在 Gin 框架中,gin.Default()
和 gin.New()
是两个不同的函数,用于创建 Gin 的引擎实例。下面进行对比:
1、gin.New()
话不多说,先上源码:
go
func New() *Engine {
debugPrintWARNINGNew() // 调用 debugPrintWARNINGNew(),可能输出一些警告信息
// 创建一个 Engine 实例,并初始化一些默认值
engine := &Engine{
//包含一些路由相关的字段和方法。
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
// 将 engine 与 RouterGroup 关联,形成完整的 Engine 结构
engine.RouterGroup.engine = engine
// 初始化一个用于 Context 对象池的 New 函数
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
函数主要的目的是创建一个 Engine
对象,初始化其默认值,设置关联关系,最后返回这个对象。这是 Gin 框架的入口,用于创建一个可以处理 HTTP 请求的引擎实例。
Engine
结构体在 Gin 框架中包装了一些 HTTP 相关的方法,它继承了 RouterGroup
结构体,而 RouterGroup
结构体中则包含了大部分的路由和中间件处理的方法:Use()、Run()、Group()等等
这些方法使得 Engine
可以直接处理 HTTP 请求,启动服务器,并具有路由、中间件等功能。Engine
通过组合和继承 RouterGroup
的方法,提供了强大而灵活的 HTTP 处理能力。
2、gin.Default()
源码:
go
func Default() *Engine {
debugPrintWARNINGDefault() //版本需要大于1.18
engine := New() //调用New()
engine.Use(Logger(), Recovery()) //添加全局日志和错误恢复
return engine
}
嗯~ 很明显了,gin.Default()
是对gin.new()
的封装、加入了局日志和错误恢复中间件。Gin 框架在默认情况下设置了全局的日志(logger)和恢复(recovery)中间件。这些中间件对于记录请求信息和恢复从 panic 中恢复的功能是非常有用的。
- Logger 中间件: Gin 使用默认的 Logger 中间件来记录每个请求的信息,包括请求方法、路径、状态码等。这使得开发者可以更容易地追踪和调试请求。默认的 Logger 中间件输出到标准输出(stdout)。
- Recovery 中间件: Gin 使用默认的 Recovery 中间件来恢复从处理程序中的 panic 中恢复。这可以防止一个处理程序中的 panic 影响整个应用程序。Recovery 中间件会将 panic 的信息记录下来,并返回一个 500 Internal Server Error 响应。
这些中间件是在 gin.Default()
中设置的。如果你使用 gin.New()
创建引擎实例,你需要手动添加这些中间件,以确保全局的日志和恢复功能。
OK,知道了New()和Default()的区别,我们来聊一聊其中的全局的日志(logger)和恢复(recovery)是怎么样运行的,换句话说,聊一聊gin的中间件的运作流程。
在这之前,提个问题: debugPrintWARNINGDefault() //版本需要大于1.18
为什么gin在设置中间件之前需要版本大于1.18.可以在评论区回复一下。
3、gin中间件
先上代码:
go
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
===============================================================
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
group.Handlers = append(group.Handlers, middleware...)
当我们看到append
的时候第一印象会想到:HandlerFunc是在切片中存放的!
OK,继续向下看
go
// HandlerFunc 定义了一个中间件函数的标准方法,入参必须带有Context
Context type HandlerFunc func(*Context)
//路由
type RouterGroup struct {
Handlers HandlersChain //函数链
basePath string //路由URL
engine *Engine //引擎
root bool
}
//这里可以看到,我们上面提到的函数就是存放在这个切片中
type HandlersChain []HandlerFunc
//HandlerFunc只有一个方法:
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
}
return nil
}
从这里就可以看到,我们的中间件是在路由上绑定的、存放在一个数组中,一个个的中间件组成函数链、函数链的构成非常简单,底层就是一个切片,对函数链的操作直接变成了对切片的操作。
OK,下面看一下RouterGroup是如何存放这些中间件的。
go
// Group 该方法会返回一个RouterGroup,并且会继承父Group的一些属性(路径、engine)
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
//将多个中间件按照顺序组合到一起
Handlers: group.combineHandlers(handlers),
//将父级的路径组合到一起 basePath:
group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
===============================================
//将一个函数链和父级的函数链结合,返回一个新的函数链、这也是多个中间件组合在一起的方式
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
//计算切片长度(函数链长度)
finalSize := len(group.Handlers) + len(handlers)
//判断切片是否超过63 (这里小编不明白为什么要设置63、有了解的在评论区评论一下)
assert1(finalSize < int(abortIndex), "too many handlers")
//定义一个新切片进行copy、返回信的切片,这样信切片内存是信的,顺序也是符合预期的
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
Ok说到这里可以看出结论了:gin的中间件主要是函数链
实现的。
但是函数链也并不是全部,它还是用了洋葱模型:
gin 框架的洋葱模型主要是通过Context 中的 Next() 和 Abort() 这两个方法来实现,我们先进行一个测试:
go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
// Middleware1
func Middleware1() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("1 Next() Before")
c.Next()
fmt.Println("1 Next() After")
}
}
// Middleware2
func Middleware2() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("2 Next() Before")
c.Next()
fmt.Println("2 Next() After")
}
}
// Middleware3
func Middleware3() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("3 Next() Before")
c.Next()
fmt.Println("3 Next() After")
}
}
func main() {
r := gin.Default()
r.Use(Middleware1(), Middleware2(), Middleware3())
r.GET("/", func(c *gin.Context) {
fmt.Println("Get request")
c.String(200, "Hello, Gin!")
})
r.Run(":8080")
}
输出:
1 Next() Before
2 Next() Before
3 Next() Before
Get request
3 Next() After
2 Next() After
1 Next() After
洋葱模型:
从第一层进去之后会先运行next直到运行到我们路由的方法后,再从内层传出去,这也是为什么gin定义了全局的Log和Recover之后,会在我们的路由方法运行完后再进行打印或者错误恢复。
Ok,那么下面看一下gin的洋葱模型是如何实现的:
go
func (engine *Engine) handleHTTPRequest(c *Context) {
...
if value.handlers != nil {
//这里将函数链和路径给context
c.handlers = value.handlers
c.fullPath = value.fullPath
//核心
c.Next()
c.writermem.WriteHeaderNow()
return
}
...
}
================================================
func (c *Context) Next() {
//index 初始值为-1
c.index++
//遍历执行所有的函数链
for c.index < int8(len(c.handlers)) {
//这里运行中间件
c.handlers[c.index](c)
//将索引向后移
c.index++
}
}
ok,所以gin的中间件主要是函数链+洋葱模型来实现的
写到这里差不多完结,New()和Default()本身都是gin项目初始化的示例的方法,如果不想使用gin提供的Log和Recover、自己重写一个使用New()也可以,本身没有什么区别。 最后提一下洋葱模型的优点吧:
- 灵活性: 洋葱模型允许你在请求进入和离开处理函数的不同阶段执行代码。这使得你可以在不同层次的中间件中处理不同的逻辑,使代码更灵活。
- 可扩展性: 新的中间件可以简单地添加到现有的中间件链中,而不需要修改现有的代码。这使得系统更容易扩展,更容易维护。
- 可重用性: 每个中间件都可以专注于一个具体的功能,使得中间件的逻辑更容易理解和复用。这有助于提高代码的可维护性和可读性。
- 顺序控制: 中间件的注册顺序决定了它们执行的顺序。这使得你可以精确地控制中间件的执行流程,确保每个中间件都在正确的时机执行。
拓展: 到了最近几年,基本上所有的框架都选择使用函数链+洋葱结构的方式来实现中间件,而有些框架使用的也有其他方式:
-
在 Express 中,中间件是基于函数的
-
在 Django 中,中间件是基于类的
-
在 ASP.NET Core 中,中间件是基于委托的
-
在Kratos中,中间件匿名函数注入的方式
你还知道那些?