深入探究 Gin:Golang 的领先框架 简介 从一个小示例开始 HTTP 方法 创建引擎变量 注册路由回调函数 使用基数树加速路由检索 导入中间件处理函数 开始运行进程消息上下文处理恐慌

2025-06-07

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

介绍

从一个小例子开始

HTTP 方法

创建引擎变量

注册路由回调函数

使用基数树加速路由检索

导入中间件处理函数

开始运行

处理消息

语境

处理恐慌

图片描述

介绍

图片描述

Gin是一个用 Go (Golang) 编写的 HTTP Web 框架。它拥有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您追求极致性能,Gin 是您的理想之选。

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

  • 快速地
  • 中间件支持
  • 无碰撞
  • 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
}
Enter fullscreen mode Exit fullscreen mode

运行这个例子,然后用浏览器访问http://localhost:8080/ping,会得到一个“pong”的声音。
这个例子很简单,可以拆分成三个步骤:

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

HTTP 方法

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

创建引擎变量

在上面的例子中,gin.Default()用于创建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
}
Enter fullscreen mode Exit fullscreen mode

现在只需简单看一下创建过程,无需关注Engine结构体中各个成员变量的含义。可以看到,除了创建并初始化一个engine类型的变量之外EngineNew还将 设置engine.pool.New为一个调用 的匿名函数engine.allocateContext()。该函数的作用稍后会讨论。

注册路由回调函数

RouterGroup内嵌了一个struct Engine,其中与HTTP方法相关的接口Engine均继承自RouterGroup,官网提到的功能点中的“路由分组”就是通过该RouterGroupstruct实现的。

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
}
Enter fullscreen mode Exit fullscreen mode

每个都RouterGroup关联一个基础路径basePathbasePath嵌入RouterGroup在 中的Engine是“/”。
此外还有一组处理函数Handlers。所有关联到该组路径下的请求都会额外执行该组的处理函数,这些函数主要用于中间件调用。Handlers在创建nilEngine,可以通过 方法来传入一组函数Use。我们稍后会看到这种用法。

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()
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

combineHandlers方法的作用是创建一个切片,然后将自身的mergedHandlers复制到其中,再将参数 复制到其中,最后返回。也就是说,使用 注册任何方法时,实际结果都包含自身的HandlersRouterGrouphandlersmergedHandlershandleHandlersRouterGroup

使用基数树加速路由检索

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

type methodTree struct {
    method string // Name of the method
    root   *node  // Pointer to the root node of the linked list
}
Enter fullscreen mode Exit fullscreen mode

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

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    //... Omit some code
    root := engine.trees.get(method)
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)
    //... Omit some code
}
Enter fullscreen mode Exit fullscreen mode

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

导入中间件处理函数

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

func Default() *Engine {
    debugPrintWARNINGDefault()       // Output log
    engine := New()                  // Create object
    engine.Use(Logger(), Recovery()) // Import middleware processing functions
    return engine
}
Enter fullscreen mode Exit fullscreen mode

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

func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}
Enter fullscreen mode Exit fullscreen mode

可以看出Use的方法也很简单,只是通过 把RouterGroup参数的中间件处理函数添加到自身中而已Handlersappend

开始运行

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

func (engine *Engine) Run(addr...string) (err error) {
    //... Omit some code
    address := resolveAddress(addr) // Parse the address, the default address is 0.0.0.0:8080
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}
Enter fullscreen mode Exit fullscreen mode

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

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
Enter fullscreen mode Exit fullscreen mode

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

处理消息

ServeHTTP的这个Engine处理消息的回调函数,我们来看一下它的内容。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) 
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c) 

    engine.pool.Put(c) 
}
Enter fullscreen mode Exit fullscreen mode

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

func (engine *Engine) handleHTTPRequest(c *Context) {
    //... Omit some code
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method!= httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        //... Omit some code
        if value.handlers!= nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        //... Omit some code
    }
    //... Omit some code
}
Enter fullscreen mode Exit fullscreen mode

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

语境

在处理 HTTP 请求时,所有与上下文相关的数据都在Context变量中。作者在Context结构体的注释中也写道“上下文是 gin 中最重要的部分”,可见其重要性。
在谈到ServeHTTP的方法时Engine,可以看出Context并不是直接创建的,而是通过变量Get的方法获取的。取出后,在使用前会重置其状态,使用完毕后会放回池中。 变量类型为。目前只需知道它是 Go 官方提供的一个支持并发使用的对象池即可。你可以通过 的方法从池中获取对象,也可以使用 的方法将对象放入池中。当池为空并使用 的方法时,它会通过自己的方法创建一个对象并返回。 这个方法定义在的方法中。我们再来看看的方法poolEngine
poolEnginesync.PoolGetPutGetNew
NewNewEngineNewEngine

func New() *Engine {
    //... Omit other code
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
Enter fullscreen mode Exit fullscreen mode

从代码中可以看出,的创建方法Context就是allocateContext的方法Engine。该方法本身并没有什么玄机allocateContext,只是做了两步切片长度的预分配,然后创建对象并返回。

func (engine *Engine) allocateContext() *Context {
    v := make(Params, 0, engine.maxParams)
    skippedNodes := make([]skippedNode, 0, engine.maxSections)
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
Enter fullscreen mode Exit fullscreen mode

Next上面提到的的方法Context执行 中的所有方法handlers。我们来看看它的实现。

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}
Enter fullscreen mode Exit fullscreen mode

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

的设计Next跟它的使用方式有很大关系,主要是为了配合一些中间件的功能。比如panic在某一个 的执行过程中触发了,就可以在中间件中handler捕获错误,然后再次调用继续执行后面的,而不会因为其中一个 的问题而影响到整个数组recoverNexthandlershandlershandler

处理恐慌

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

func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
    //... Omit other code
    return func(c *Context) {
        defer func() {
            if err := recover(); err!= nil {
                //... Error handling code
            }
        }()
        c.Next() // Execute the next handler
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

图片描述

1.多语言支持

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

2. 免费部署无限项目

  • 仅按使用量付费 — 无请求,无费用。

3.无与伦比的成本效益

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

4. 简化的开发人员体验

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

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

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

在文档中探索更多

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

文章来源:https://dev.to/leapcell/a-deep-dive-into-gin-golangs-leading-framework-5e39
PREV
Event-Driven Architecture in Node.js
NEXT
在 Bash 中模拟 OOP