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中,中间件匿名函数注入的方式

你还知道那些?

相关推荐
郝同学的测开笔记18 小时前
云原生探索系列(十二):Go 语言接口详解
后端·云原生·go
一点一木1 天前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
懒阳羊2 天前
Gin框架
gin
千羽的编程时光2 天前
【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 实战
go
TMDOG6662 天前
TMDOG的Gin学习笔记_02——Gin集成支付宝支付沙箱环境
笔记·学习·gin
5pace2 天前
GIN:逼近WL-test的GNN架构
gin
RationalDysaniaer3 天前
gin入门
笔记·gin
清北_3 天前
Go常见框架对比
前端·golang·gin
还是转转3 天前
Go开发指南-Gin与Web开发
golang·gin
27669582923 天前
阿里1688 阿里滑块 231滑块 x5sec分析
java·python·go·验证码·1688·阿里滑块·231滑块