Gin的初始化New()和Default()

在 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 中恢复的功能是非常有用的。

  1. Logger 中间件: Gin 使用默认的 Logger 中间件来记录每个请求的信息,包括请求方法、路径、状态码等。这使得开发者可以更容易地追踪和调试请求。默认的 Logger 中间件输出到标准输出(stdout)。
  2. 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()也可以,本身没有什么区别。 最后提一下洋葱模型的优点吧:

  1. 灵活性: 洋葱模型允许你在请求进入和离开处理函数的不同阶段执行代码。这使得你可以在不同层次的中间件中处理不同的逻辑,使代码更灵活。
  2. 可扩展性: 新的中间件可以简单地添加到现有的中间件链中,而不需要修改现有的代码。这使得系统更容易扩展,更容易维护。
  3. 可重用性: 每个中间件都可以专注于一个具体的功能,使得中间件的逻辑更容易理解和复用。这有助于提高代码的可维护性和可读性。
  4. 顺序控制: 中间件的注册顺序决定了它们执行的顺序。这使得你可以精确地控制中间件的执行流程,确保每个中间件都在正确的时机执行。

拓展: 到了最近几年,基本上所有的框架都选择使用函数链+洋葱结构的方式来实现中间件,而有些框架使用的也有其他方式:

  1. 在 Express 中,中间件是基于函数的

  2. 在 Django 中,中间件是基于类的

  3. ASP.NET Core 中,中间件是基于委托的

  4. 在Kratos中,中间件匿名函数注入的方式

你还知道那些?

相关推荐
代码扳手7 小时前
Go + gRPC + HTTP/3:解锁下一代高性能通信
go
mit6.82410 小时前
[OP-Agent] `opa run` | 交互模式(REPL) | 服务模式(HTTP API) | runtime包
go·1024程序员节
用户9230777112241 天前
Gin教程 Golang+Gin框架入门实战教程 大地老师
gin
梁梁梁梁较瘦2 天前
边界检查消除(BCE,Bound Check Elimination)
go
梁梁梁梁较瘦2 天前
指针
go
梁梁梁梁较瘦2 天前
内存申请
go
半枫荷2 天前
七、Go语法基础(数组和切片)
go
梁梁梁梁较瘦3 天前
Go工具链
go
半枫荷3 天前
六、Go语法基础(条件控制和循环控制)
go
半枫荷4 天前
五、Go语法基础(输入和输出)
go