首页 > 后端开发 > Golang > 深入探讨 Gin:Golang 的领先框架

深入探讨 Gin:Golang 的领先框架

Linda Hamilton
发布: 2025-01-01 05:16:12
原创
661 人浏览过

A Deep Dive into Gin: Golang

介绍

A Deep Dive into Gin: Golang

Gin 是一个用 Go(Golang)编写的 HTTP Web 框架。它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您需要精彩的表演,那就给自己来点杜松子酒吧。

Gin 官网介绍自己是一个具有“高性能”和“良好生产力”的 Web 框架。它还提到了另外两个库。第一个是Martini,它也是一个Web框架,并且有一个酒的名字。 Gin 表示它使用其 API,但速度快了 40 倍。使用httprouter是它能比Martini快40倍的重要原因。
官网的“Features”中列出了八个关键功能,稍后我们将逐步看到这些功能的实现。

  • 中间件支持
  • 无崩溃
  • JSON 验证
  • 路线分组
  • 错误管理
  • 渲染内置/可扩展

从一个小例子开始

让我们看一下官方文档中给出的最小示例。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

运行这个例子,然后用浏览器访问http://localhost:8080/ping,就会得到“乒乓”声。
这个例子非常简单。它可以分为三个步骤:

  1. 使用 gin.Default() 创建具有默认配置的 Engine 对象。
  2. 在Engine的GET方法中注册“/ping”地址的回调函数。此函数将返回一个“pong”。
  3. 启动Engine,开始监听端口并提供服务。

HTTP方法

从上面小例子中的GET方法我们可以看出,在Gin中,HTTP方法的处理方法需要使用对应的同名函数进行注册。
HTTP 方法有九种,最常用的四种是 GET、POST、PUT 和 DELETE,分别对应查询、插入、更新和删除四种功能。需要注意的是,Gin还提供了Any接口,可以直接将所有HTTP方法处理方法绑定到一个地址。
返回的结果一般包含两部分或三部分。代码和消息始终存在,数据通常用于表示附加数据。如果没有额外的数据要返回,则可以省略。在示例中,200 是 code 字段的值,“pong”是 message 字段的值。

创建引擎变量

在上面的示例中,gin.Default() 用于创建引擎。然而,这个函数是 New 的包装。其实Engine就是通过New接口创建的。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

暂时简单看一下创建过程,不要关注Engine结构体中各个成员变量的含义。可以看到,New除了创建并初始化一个Engine类型的引擎变量外,还将engine.pool.New设置为一个调用engine.allocateContext()的匿名函数。这个函数的作用后面会讲。

注册路由回调函数

Engine 中有一个嵌入的 struct RouterGroup。 Engine的HTTP方法相关接口均继承自RouterGroup。官网提到的功能点中的“路由分组”是通过RouterGroup结构体实现的。

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            //... Initialize the fields of RouterGroup
        },
        //... Initialize the remaining fields
    }
    engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
登录后复制
登录后复制
登录后复制
登录后复制

每个 RouterGroup 都与一个基本路径 basePath 相关联。 Engine 中嵌入的 RouterGroup 的基本路径是“/”。
还有一组处理函数Handlers。所有与该组关联的路径下的请求都会额外执行该组的处理函数,主要用于中间件调用。 Engine创建时Handlers为nil,可以通过Use方法导入一组函数。我们稍后会看到这个用法。

type RouterGroup struct {
    Handlers    HandlersChain // Processing functions of the group itself
    basePath    string        // Associated base path
    engine      *Engine       // Save the associated engine object
    root        bool          // root flag, only the one created by default in Engine is true
}
登录后复制
登录后复制
登录后复制

RouterGroup的handle方法是注册所有HTTP方法回调函数的最终入口。初始示例中调用的 GET 方法和其他与 HTTP 方法相关的方法只是对 handle 方法的包装。
handle方法会根据RouterGroup的basePath和相对路径参数计算出绝对路径,同时调用combineHandlers方法得到最终的handlers数组。这些结果作为参数传递给Engine的addRoute方法来注册处理函数。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}
登录后复制
登录后复制
登录后复制

combineHandlers方法的作用是创建一个切片mergedHandlers,然后将RouterGroup本身的Handler复制到其中,然后将参数的handler复制到其中,最后返回mergedHandlers。也就是说,当使用handle注册任何方法时,实际结果包括RouterGroup本身的Handler。

使用Radix Tree加速路由检索

在官网提到的“快速”特征点中,提到网络请求的路由是基于基数树(Radix Tree)实现的。这部分并不是由Gin实现的,而是由一开始介绍Gin时提到的httprouter实现的。 Gin使用httprouter来实现这部分功能。基数树的实现这里暂时不讲。我们现在只关注它的用法。也许稍后我们会单独写一篇文章来介绍基数树的实现。
在引擎中,有一个 trees 变量,它是 methodTree 结构的一个切片。正是这个变量保存了对所有基数树的引用。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

引擎为每个 HTTP 方法维护一个基数树。这棵树的根节点和方法的名称一起保存在一个methodTree变量中,所有methodTree变量都在树中。

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            //... Initialize the fields of RouterGroup
        },
        //... Initialize the remaining fields
    }
    engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
登录后复制
登录后复制
登录后复制
登录后复制

可以看到,Engine的addRoute方法中,首先会使用trees的get方法来获取该方法对应的radix树的根节点。如果没有获取到基数树的根节点,则说明之前没有为该方法注册过任何方法,将会创建一个树节点作为树的根节点,并添加到树中。
获取根节点后,使用根节点的addRoute方法注册一组针对路径path的处理函数handler。这一步是为路径和处理程序创建一个节点并将其存储在基数树中。如果你尝试注册一个已经注册的地址,addRoute会直接抛出一个panic错误。
在处理HTTP请求时,需要通过路径找到对应节点的值。根节点有一个getValue方法负责处理查询操作。我们在谈论 Gin 处理 HTTP 请求时会提到这一点。

导入中间件处理函数

RouterGroup的Use方法可以导入一组中间件处理函数。官网提到的功能点中的“中间件支持”是通过Use方法实现的。
在最初的示例中,创建Engine结构体变量时,没有使用New,而是使用了Default。让我们看看 Default 额外做了什么。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

可以看出,这是一个非常简单的函数。除了调用New创建Engine对象外,只调用Use导入Logger和Recovery两个中间件函数的返回值。 Logger的返回值是用于记录日志的函数,Recovery的返回值是用于处理panic的函数。我们暂时跳过这个,稍后再看这两个函数。
虽然Engine内嵌了RouterGroup,也实现了Use方法,但只是调用了RouterGroup的Use方法以及一些辅助操作。

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            //... Initialize the fields of RouterGroup
        },
        //... Initialize the remaining fields
    }
    engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
登录后复制
登录后复制
登录后复制
登录后复制

可见RouterGroup的使用方法也非常简单。它只是通过append将参数的中间件处理功能添加到自己的Handler中。

开始跑步

在这个小例子中,最后一步是不带参数调用 Engine 的 Run 方法。调用后,整个框架开始运行,用浏览器访问注册地址即可正确触发回调。

type RouterGroup struct {
    Handlers    HandlersChain // Processing functions of the group itself
    basePath    string        // Associated base path
    engine      *Engine       // Save the associated engine object
    root        bool          // root flag, only the one created by default in Engine is true
}
登录后复制
登录后复制
登录后复制

Run方法只做两件事:解析地址和启动服务。这里地址其实只需要传一个字符串就可以了,但是为了达到能传能不能传的效果,使用了一个可变参数。 resolveAddress方法处理addr不同情况的结果。
启动服务使用标准库的net/http包中的ListenAndServe方法。该方法接受一个监听地址和一个Handler接口的变量。 Handler接口的定义非常简单,只有一个ServeHTTP方法。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}
登录后复制
登录后复制
登录后复制

因为Engine实现了ServeHTTP,所以Engine本身将被传递到这里的ListenAndServe方法。当监听端口有新的连接时,ListenAndServe会负责接受并建立连接,当连接上有数据时,会调用handler的ServeHTTP方法进行处理。

处理消息

Engine的ServeHTTP是处理消息的回调函数。我们来看看它的内容。

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    assert1(finalSize < int(abortIndex), "too many handlers")
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}
登录后复制

回调函数有两个参数。第一个是w,用于接收请求回复。将回复数据写入w。另一个是req,保存本次请求的数据。后续处理所需的所有数据都可以从req中读取
ServeHTTP 方法做了四件事。首先从pool池中获取一个Context,然后将Context与回调函数的参数绑定,然后以Context为参数调用handleHTTPRequest方法来处理这次网络请求,最后将Context放回池中。
我们首先只看handleHTTPRequest方法的核心部分。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

handleHTTPRequest方法主要做了两件事。首先根据请求的地址从基数树中获取之前注册的方法。这里,handlers会被分配到Context中进行本次处理,然后调用Context的Next函数来执行handlers中的方法。最后将本次请求的返回数据写入Context的responseWriter类型对象中。

语境

处理 HTTP 请求时,所有与上下文相关的数据都在 Context 变量中。作者还在Context结构体的注释中写到“Context is the most important part of gin”,可见其重要性。
上面讲Engine的ServeHTTP方法时可以看出,Context并不是直接创建的,而是通过Engine的pool变量的Get方法获取的。取出后,使用前重置其状态,使用后放回池中。
Engine 的池变量的类型为sync.Pool。目前只知道它是Go官方提供的支持并发使用的对象池。您可以通过 Get 方法从池中获取对象,也可以使用 Put 方法将对象放入池中。当池为空并且使用Get方法时,它会通过自己的New方法创建一个对象并返回。
这个New方法是在Engine的New方法中定义的。我们再看一下Engine的New方法。

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            //... Initialize the fields of RouterGroup
        },
        //... Initialize the remaining fields
    }
    engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
登录后复制
登录后复制
登录后复制
登录后复制

从代码中可以看出Context的创建方法是Engine的allocateContext方法。 allocateContext 方法并没有什么神秘之处。它只是对切片长度进行两步预分配,然后创建对象并返回它。

type RouterGroup struct {
    Handlers    HandlersChain // Processing functions of the group itself
    basePath    string        // Associated base path
    engine      *Engine       // Save the associated engine object
    root        bool          // root flag, only the one created by default in Engine is true
}
登录后复制
登录后复制
登录后复制

上面提到的 Context 的 Next 方法将执行处理程序中的所有方法。我们来看看它的实现。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}
登录后复制
登录后复制
登录后复制

虽然handlers是一个切片,但是Next方法并不是简单地实现为handlers的遍历,而是引入了一个处理进度记录索引,该索引初始化为0,在方法开始时递增,在方法结束后再次递增执行完成。

Next的设计和它的用法有很大关系,主要是为了配合一些中间件功能。例如,当某个handler执行过程中触发panic时,可以使用中间件中的recover捕获错误,然后再次调用Next继续执行后续的handler,而不会因为该问题影响整个handlers数组一名处理程序。

应对恐慌

在Gin中,如果某个请求的处理函数触发了panic,整个框架并不会直接崩溃。相反,将抛出错误消息,并且将继续提供服务。这有点类似于Lua框架通常使用xpcall来执行消息处理函数。这个操作就是官方文档中提到的“Crash-free”特性点。
上面提到,使用 gin.Default 创建 Engine 时,会执行 Engine 的 Use 方法来导入两个函数。其中之一是 Recovery 函数的返回值,它是其他函数的包装。最终调用的函数是CustomRecoveryWithWriter。我们来看看这个函数的实现。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

这里我们不关注错误处理的细节,而只看看它做了什么。该函数返回一个匿名函数。在这个匿名函数中,使用defer注册了另一个匿名函数。在这个内部匿名函数中,使用recover来捕获panic,然后进行错误处理。处理完成后,调用Context的Next方法,这样Context原本按顺序执行的处理程序就可以继续执行。

Leapcell:用于 Web 托管、异步任务和 Redis 的下一代无服务器平台

最后给大家介绍一下部署Gin服务最好的平台:Leapcell。

A Deep Dive into Gin: Golang

1. 多语言支持

  • 使用 JavaScript、Python、Go 或 Rust 进行开发。

2.免费部署无限个项目

  • 只需支付使用费用——无请求,不收费。

3. 无与伦比的成本效益

  • 即用即付,无闲置费用。
  • 示例:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。

4.简化的开发者体验

  • 直观的用户界面,轻松设置。
  • 完全自动化的 CI/CD 管道和 GitOps 集成。
  • 实时指标和日志记录以获取可操作的见解。

5. 轻松的可扩展性和高性能

  • 自动扩展以轻松处理高并发。
  • 零运营开销——只需专注于构建。

在文档中探索更多内容!

Leapcell Twitter:https://x.com/LeapcellHQ

以上是深入探讨 Gin:Golang 的领先框架的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板