gin框架底层

news2025/1/23 17:54:21

gin框架底层

gin的背景和使用

在这里插入图片描述

  • 这里蓝色的是gin增强的内容,红色的是为了支持增强的内容添加的东西,黄色的是原来的net/http库
  • Gin框架是基于Go语言的net/http标准库构建的,它提供了一个gin.Engine对象,这个对象实现了http.Handler接口,handler是路径处理函数。在Go中,http.Handler接口定义了一个ServeHTTP方法,该方法接受一个http.ResponseWriter和一个*http.Request作为参数。当你定义了一个gin.Engine实例,并设置了相应的路由和处理函数后,Gin会自动处理HTTP请求。gin.Engine对象内部会根据请求的URL和其他参数来匹配路由,并调用相应的处理函数。这个处理函数会接收一个*gin.Context参数,而不是直接接收http.ResponseWriter和*http.Request。gin.Context封装了http.ResponseWriter和*http.Request,并提供了更多的功能,如上下文数据存储、中间件支持等。
  • 确切地说,在 Gin 框架中,“处理函数”(handler)通常指的是路由处理函数,即直接处理 HTTP 请求并生成响应的函数。这些路由处理函数是通过路由注册的方式来绑定到具体的 URL 路径上,当用户请求匹配的 URL 路径时,相应的处理函数就会被调用。中间件是一种特殊的处理函数,它们可以在路由处理函数被调用之前或之后执行一些操作。中间件通常被用来处理一些公共逻辑,比如日志记录、身份验证、请求参数解析等。在 Gin 框架中,中间件的签名也是 func(*gin.Context),因此在技术上,中间件也可以被看作是一种处理函数。
  • Gin 框架支持中间件的使用,通过 gin.Engine 的 Use() 方法添加的中间件会作用于所有的请求。
代码如下
<!-- code_example -->
// • 构造 gin.Engine 实例:gin.Default()
// • 路由组注册中间件:Engine.Use()
// • 路由组注册 POST 方法下的 handler:Engine.POST()
// • 启动 http server:Engine.Run()
import "github.com/gin-gonic/gin"


func main() {
    // 创建一个 gin Engine,本质上是一个 http Handler
    mux := gin.Default()
    // 注册中间件
    var myMiddleWare = func(mux *gin.Context){
        fmt.Println("start")
    }
    mux.Use(myMiddleWare)
    // 注册一个 path 为 /ping 的处理函数
    mux.POST("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, "pone")
    })
    // 运行 http 服务
    if err := mux.Run(":8080"); err != nil {
        panic(err)
    }
}


gin.Engine核心数据结构

  • 如下图所示

在这里插入图片描述

  • sync.Pool:对象池,复用gin.Context,给这个实例,并发的时候会使用和销毁很多gin.Context,因此先不去内存创造gin.Context,去回收站里看看有没有可以用的gin.Context,后面再讨论
  • routergroup:路由组,针对每一个请求有一个路径处理函数,比如我的 api 路由组定义了两个路由:/api/ping 和 /api/users。这意味着所有以 /api/ 开头的请求都会被路由到这个组内的相应处理函数。

type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine *Engine
    root bool
}

// RouterGroup 是路由组的概念,其中的配置将被从属于该路由组的所有路由复用:

// • Handlers:路由组共同的 handler 处理函数链. 组下的节点将拼接 RouterGroup 的公用 handlers 和自己的 handlers,组成最终使用的 handlers 链
// • basePath:路由组的基础路径. 组下的节点将拼接 RouterGroup 的 basePath 和自己的 path,组成最终使用的 absolutePath
// • engine:指向路由组从属的 Engine
// • root:标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false

---------------------------------

type HandlersChain []HandlerFunc


type HandlerFunc func(*Context)
// HandlersChain 是由多个路由处理函数 HandlerFunc 构成的处理函数链. 在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc.

  • trees:路由树,根据方法有很多树
  • gin.Logger() 和 gin.Recovery()实现的是gin.HandlerFunc,这个类型返回包含*gin.context,所以调用的时候无需传入 *gin.context

注册handler流程

  • 以上面的代码为例,进行流程分析

初始化Engine

// 方法调用:gin.Default -> gin.New

// • 创建一个 gin.Engine 实例
// • 创建 Enging 的首个 RouterGroup,对应的处理函数链 Handlers 为 nil,基础路径 basePath 为 "/",root 标识为 true
// • 构造了 9 棵方法路由树,对应于 9 种 http 方法
// • 创建了 gin.Context 的对象池

//下面是gin.New()的源码

func Default() *Engine {
    engine := New()
    // ...
    return engine
}
 

func New() *Engine {
    // ...
    // 创建 gin Engine 实例
    engine := &Engine{
        // 路由组实例
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        // ...
        // 9 棵路由压缩前缀树,对应 9 种 http 方法
        trees:                  make(methodTrees, 0, 9),
        // ...
    }
    engine.RouterGroup.engine = engine     
    // gin.Context 对象池   
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}

注册middleware(中间件)


// 通过 Engine.Use 方法可以实现中间件的注册,会将注册的 middlewares 添加到 RouterGroup.Handlers 中. 后续 RouterGroup 下新注册的 handler 都会在前缀中拼上这部分 group 公共的 handlers.

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

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

注册handler

  • 注册 handler 的意思是将处理函数(包括中间件函数)与特定的 URL 路径相关联,并将其加载到路由树上。
  • 这里给出一些解释,参考下面这张流程图,以POST为例
  • 以一次性注册多个处理函数开始,以链条的形式,然后到后面有三个步骤,分别是
    • 第一步,看当前路径是属于哪个路由组,有一些属性,先看routergroup,先获取basepath,然后拼接自己相对路径,得到完整绝对路径,
    • 然后在第二步的时候,注册具体路径的handlers时,需要先把组公有的handlers连接在一起,获得完整的链
    • 最后会走到九个方法树,找到自己的树,这里是post,到压缩前缀树,根据绝对路径,做一个检索的过程,找这个节点,如果有就用,没有就创建,然后将刚才的handlers链挂载上去。

在这里插入图片描述

  • 下面的代码是上面解释的一些源码

// 以 http post 为例,注册 handler 方法调用顺序为 RouterGroup.POST-> RouterGroup.handle,接下来会完成三个步骤:

// • 拼接出待注册方法的完整路径 absolutePath
// • 拼接出代注册方法的完整处理函数链 handlers
// • 以 absolutePath 和 handlers 组成 kv 对添加到路由树中
// 下面依次是源码
 

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodPost, relativePath, handlers)
}
 

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)//拼接绝对路径
    handlers = group.combineHandlers(handlers)//拼接所有handlers
    group.engine.addRoute(httpMethod, absolutePath, handlers)//挂载到树上
    return group.returnObj()


// (1)完整路径拼接
// 结合 RouterGroup 中的 basePath 和注册时传入的 relativePath,组成 absolutePath

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}
 

func joinPaths(absolutePath, relativePath string) string {
    if relativePath == "" {
        return absolutePath
    }


    finalPath := path.Join(absolutePath, relativePath)
    if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
        return finalPath + "/"
    }
    return finalPath
}
 

// (2)完整 handlers 生成
// 深拷贝 RouterGroup 中 handlers 和注册传入的 handlers,生成新的 handlers 数组并返回

 

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
}
 

// (3)注册 handler 到路由树
// • 获取 http method 对应的 methodTree
// • 将 absolutePath 和对应的 handlers 注册到 methodTree 中
// 路由注册方法 root.addRoute 的信息量比较大,放在本文第 4 章中详细拆解.

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    // ...
    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)
    // ...
}

gin启动服务端流程

  • 以上面的代码为例,进行流程分析

启动服务

// 一键启动 Engine.Run 方法后,底层会将 gin.Engine 本身作为 net/http 包下 Handler interface 的实现类,并调用 http.ListenAndServe 方法启动服务.

func (engine *Engine) Run(addr ...string) (err error) {
    // ...
    err = http.ListenAndServe(address, engine.Handler())//这里传入的是gin.engine创造的handler,如果不传,则是默认的net包里的
    return
}
   

// ListenerAndServe 方法本身会基于主动轮询 + IO 多路复用的方式运行,因此程序在正常运行时,会始终阻塞于 Engine.Run 方法,不会返回.



func (srv *Server) Serve(l net.Listener) error {
   // ...
   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()//判断是否有客户端的请求
        // ...
        connCtx := ctx
        // ...
        c := srv.newConn(rw)
        // ...
        go c.serve(connCtx)
    }
}

 
  • 下图就是上面描述的过程

在这里插入图片描述

处理请求

  • 下图是 engine.handleHTTPRequest 处理请求的流程图,找到对应的树,根据路径找到handler链。然后响应请求

在这里插入图片描述

// 在服务端接收到 http 请求时,会通过 Handler.ServeHTTP 方法进行处理. 而此处的 Handler 正是 gin.Engine,其处理请求的核心步骤如下:

// • 对于每笔 http 请求,会为其分配一个 gin.Context,在 handlers 链路中持续向下传递
// • 调用 Engine.handleHTTPRequest 方法,从路由树中获取 handlers 链,然后遍历调用
// • 处理完 http 请求后,会将 gin.Context 进行回收. 整个回收复用的流程基于对象池管理
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从对象池中获取一个 context
    c := engine.pool.Get().(*Context)
    
    // 重置/初始化 context
    c.writermem.reset(w)
    c.Request = req
    c.reset()
    
    // 处理 http 请求
    engine.handleHTTPRequest(c)


    // 把 context 放回对象池
    engine.pool.Put(c)
}
 



 

// Engine.handleHTTPRequest 方法核心步骤分为三步:

// • 根据 http method 取得对应的 methodTree
// • 根据 path 从 methodTree 中找到对应的 handlers 链
// • 将 handlers 链注入到 gin.Context 中,通过 Context.Next 方法按照顺序遍历调用 handler
// 此处根据 path 从路由树寻找 handlers 的逻辑位于 root.getValue 方法中,和路由树数据结构有关,放在后面详解;

// 根据 gin.Context.Next 方法遍历 handler 链的内容放在也放在后面详解.

func (engine *Engine) handleHTTPRequest(c *Context) { //Context有请求的信息,还有handler链条
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    
    // ...
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        // 获取对应的方法树
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // 从路由树中寻找路由
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        // ...
        break
    }
    // ...


Gin的路由树

策略与原理

  • radix tree,下面就是压缩前缀树,因为一个节点是几个字符

在这里插入图片描述

前缀树

  • 前缀树又称 trie 树,是一种基于字符串公共前缀构建索引的树状结构,核心点包括:

• 除根节点之外,每个节点对应一个字符
• 从根节点到某一节点,路径上经过的字符串联起来,即为该节点对应的字符串
• 尽可能复用公共前缀,如无必要不分配新的节点
tries 树在 leetcode 上的题号为 208,手动实现一下.

压缩前缀树

  • 压缩前缀树又称基数树或 radix 树,是对前缀树的改良版本,优化点主要在于空间的节省,核心策略体现在:倘若某个子节点是其父节点的唯一孩子,且这个父节点不是某个字符串的结尾,则与父节点进行合并
  • 在 gin 框架中,使用的正是压缩前缀树的数据结构.

为什么使用压缩前缀树

  • 与压缩前缀树相对的就是使用 hashmap,以 path 为 key,handlers 为 value 进行映射关联,这里选择了前者的原因在于:
    • path 匹配时不是完全精确匹配,比如末尾 ‘/’ 符号的增减、全匹配符号 ‘*’ 的处理等,map 无法胜任(模糊匹配部分的代码于本文中并未体现,大家可以深入源码中加以佐证)
    • 路由的数量相对有限,对应数量级下 map 的性能优势体现不明显,在小数据量的前提下,map 性能甚至要弱于前缀树
    • path 串通常存在基于分组分类的公共前缀,适合使用前缀树进行管理,可以节省存储空间

补偿策略

  • 在 Gin 路由树中还使用一种补偿策略,在组装路由树时,会将注册路由句柄数量更多的 child node 摆放在 children 数组更靠前的位置,也就是最左边的位置(如果优先从左边找的话)

  • 这是因为某个链路注册的 handlers 句柄数量越多,一次匹配操作所需要花费的时间就越长,且被匹配命中的概率就越大,因此应该被优先处理,如下图所示

在这里插入图片描述

核心数据结构

在这里插入图片描述


// 路由树的数据结构,对应于 9 种 http method,共有 9 棵 methodTree. 每棵 methodTree 会通过 root 指向 radix tree 的根节点.

 

type methodTree struct {
    method string
    root   *node
}
 

// node 是 radix tree 中的节点,对应节点含义如下:

// • path:节点的相对路径,拼接上 RouterGroup 中的 basePath 作为前缀后才能拿到完整的路由 path
// • indices:由各个子节点 path 首字母组成的字符串,子节点顺序会按照途径的路由数量 priority进行排序
// • priority:途径本节点的路由数量,反映出本节点在父节点中被检索的优先级
// • children:子节点列表
// • handlers:当前节点对应的处理函数链
 

type node struct {
    // 节点的相对路径
    path string
    // 每个 indice 字符对应一个孩子节点的 path 首字母,和children []*node 这里对应
    indices string
    // ...
    // 后继节点数量
    priority uint32 //越多越靠前
    // 孩子节点列表
    children []*node 
    // 处理函数链
    handlers HandlersChain
    // path 拼接上前缀后的完整路径
    fullPath string
}


路由树的注册

这里是与上面对应的注册handler的第三步,注册handler到路由

// 下述代码展示了将一组 path + handlers 添加到 radix tree 的详细过程,核心位置均已给出注释
// 插入新路由
func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path
    // 每有一个新路由经过此节点,priority 都要加 1
    n.priority++


    // 加入当前节点为 root 且未注册过子节点,则直接插入由并返回
    if len(n.path) == 0 && len(n.children) == 0 {
        n.insertChild(path, fullPath, handlers)
        n.nType = root
        return
    }


// 外层 for 循环断点
walk://标签
    for {
        // 获取 node.path 和待插入路由 path 的最长公共前缀长度
        i := longestCommonPrefix(path, n.path)
    
        // 倘若最长公共前缀长度小于 node.path 的长度,代表 node 需要分裂
        // 举例而言:node.path = search,此时要插入的 path 为 see
        // 最长公共前缀长度就是 2,len(n.path) = 6
        // 需要分裂为  se -> arch
        //               -> e    
        if i < len(n.path) {
        // 原节点分裂后的后半部分,对应于上述例子的 arch 部分
            child := node{
                path:      n.path[i:],
                // 原本 search 对应的参数都要托付给 arch
                indices:   n.indices,
                children: n.children,              
                handlers:  n.handlers,
                // 新路由 see 进入时,先将 search 的 priority 加 1 了,此时需要扣除 1 并赋给 arch
                priority:  n.priority - 1,
                fullPath:  n.fullPath,
            }


            // 先建立 search -> arch 的数据结构,后续调整 search 为 se
            n.children = []*node{&child}
            // 设置 se 的 indice 首字母为 a
            n.indices = bytesconv.BytesToString([]byte{n.path[i]})
            // 调整 search 为 se
            n.path = path[:i]
            // search 的 handlers 都托付给 arch 了,se 本身没有 handlers
            n.handlers = nil           
            // ...
        }


        // 最长公共前缀长度小于 path,正如 se 之于 see
        if i < len(path) {
            // path see 扣除公共前缀 se,剩余 e
            path = path[i:]
            c := path[0]            


            // 根据 node.indices,辅助判断,其子节点中是否与当前 path 还存在公共前缀       
            for i, max := 0, len(n.indices); i < max; i++ {
               // 倘若 node 子节点还与 path 有公共前缀,则令 node = child,并调到外层 for 循环 walk 位置开始新一轮处理
                if c == n.indices[i] {                   
                    i = n.incrementChildPrio(i)
                    n = n.children[i]
                    continue walk
                }
            }
            
            // node 已经不存在和 path 再有公共前缀的子节点了,则需要将 path 包装成一个新 child node 进行插入      
            // node 的 indices 新增 path 的首字母    
            n.indices += bytesconv.BytesToString([]byte{c})
            // 把新路由包装成一个 child node,对应的 path 和 handlers 会在 node.insertChild 中赋值
            child := &node{
                fullPath: fullPath,
            }
            // 新 child node append 到 node.children 数组中
            n.addChild(child)
            n.incrementChildPrio(len(n.indices) - 1)
            // 令 node 指向新插入的 child,并在 node.insertChild 方法中进行 path 和 handlers 的赋值操作
            n = child          
            n.insertChild(path, fullPath, handlers)
            return
        }


        // 此处的分支是,path 恰好是其与 node.path 的公共前缀,则直接复制 handlers 即可
        // 例如 se 之于 search
        if n.handlers != nil {
            panic("handlers are already registered for path '" + fullPath + "'")
        }
        n.handlers = handlers
        // ...
        return
}  
 
}
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
    // ...
    n.path = path
    n.handlers = handlers
    // ...
}
 

// 呼应谈到的补偿策略,下面这段代码体现了,在每个 node 的 children 数组中,child node 在会依据 priority 有序排列,保证 priority 更高的 child node 会排在数组前列,被优先匹配.

func (n *node) incrementChildPrio(pos int) int {
    cs := n.children
    cs[pos].priority++
    prio := cs[pos].priority




    // Adjust position (move to front)
    newPos := pos
    for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
        // Swap node positions
        cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
    }




    // Build new index char string
    if newPos != pos {
        n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
            n.indices[pos:pos+1] + // The index char we move
            n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
    }




    return newPos
}


检索路由树

这里是与上面对应的处理请求,Engine.handleHTTPRequest()


type nodeValue struct {
    // 处理函数链
    handlers HandlersChain
    // ...
}
 

// 从路由树中获取 path 对应的 handlers 
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
    var globalParamsCount int16


// 外层 for 循环断点
walk: 
    for {
        prefix := n.path
        // 待匹配 path 长度大于 node.path
        if len(path) > len(prefix) {
            // node.path 长度 < path,且前缀匹配上
            if path[:len(prefix)] == prefix {
                // path 取为后半部分
                path = path[len(prefix):]
                // 遍历当前 node.indices,找到可能和 path 后半部分可能匹配到的 child node
                idxc := path[0]
                for i, c := range []byte(n.indices) {
                    // 找到了首字母匹配的 child node
                    if c == idxc {
                        // 将 n 指向 child node,调到 walk 断点开始下一轮处理
                        n = n.children[i]
                        continue walk
                    }
                }


                // ...
            }
        }


        // 倘若 path 正好等于 node.path,说明已经找到目标
        if path == prefix {
            // ...
            // 取出对应的 handlers 进行返回 
            if value.handlers = n.handlers; value.handlers != nil {
                value.fullPath = n.fullPath
                return
            }


            // ...           
        }


        // 倘若 path 与 node.path 已经没有公共前缀,说明匹配失败,会尝试重定向,此处不展开
        // ...
 }  

}

Gin.Context

核心数据结构

在这里插入图片描述



// gin.Context 的定位是对应于一次 http 请求,贯穿于整条 handlersChain 调用链路的上下文,其中包含了如下核心字段:

// • Request/Writer:http 请求和响应的 reader(读取数据)、writer(处理完后回馈到客户端) 入口
// • handlers:本次 http 请求对应的处理函数链
// • index:当前的处理进度,即处理链路处于函数链的索引位置
// • engine:Engine 的指针
// • mu:用于保护 map 的读写互斥锁
// • Keys:缓存 handlers 链上共享数据的 map
type Context struct {
    // ...
    // http 请求参数
    Request   *http.Request
    // http 响应 writer
    Writer    ResponseWriter
    // ...
    // 处理函数链
    handlers HandlersChain
    // 当前处于处理函数链的索引
    index    int8
    engine       *Engine
    // ...
    // 读写锁,保证并发安全
    mu sync.RWMutex
    // key value 对存储 map
    Keys map[string]any
    // ..
}


复用策略

在这里插入图片描述


// gin.Context 作为处理 http 请求的通用数据结构,不可避免地会被频繁创建和销毁. 为了缓解 GC 压力,gin 中采用对象池 sync.Pool 进行 Context 的缓存复用,处理流程如下:

// • http 请求到达时,从 pool 中获取 Context,倘若池子已空,通过 pool.New 方法构造新的 Context 补上空缺
// • http 请求处理完成后,将 Context 放回 pool 中,用以后续复用
// sync.Pool 并不是真正意义上的缓存,将其称为回收站或许更加合适,放入其中的数据在逻辑意义上都是已经被删除的,但在物理意义上数据是仍然存在的,这些数据可以存活两轮 GC 的时间,在此期间倘若有被获取的需求,则可以被重新复用.
 
// goroutine和调度器P进行绑定来调度,对每一个P会有一个private(一个)和sharedlist(共享的),P调度运行的时候先去看private有没有,要是有的话,直接用,不需要加锁,因为就一个,要是没有的话,去shared list看有没有能用的,需要加锁,要是没有,就去其他调度器里的sharedlist看看有没有能用的,没有的话就new新的



type Engine struct {
    // context 对象池
    pool             sync.Pool
}
 

func New() *Engine {
    // ...
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}
 

func (engine *Engine) allocateContext(maxParams uint16) *Context {
    v := make(Params, 0, maxParams)
   // ...
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}






分配与回收

在这里插入图片描述


// gin.Context 分配与回收的时机是在 gin.Engine 处理 http 请求的前后,位于 Engine.ServeHTTP 方法当中:

// • 从池中获取 Context
// • 重置 Context 的内容,使其成为一个空白的上下文
// • 调用 Engine.handleHTTPRequest 方法处理 http 请求
// • 请求处理完成后,将 Context 放回池中
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从对象池中获取一个 context
    c := engine.pool.Get().(*Context)
    // 重置/初始化 context
    c.writermem.reset(w)
    c.Request = req
    c.reset()
    // 处理 http 请求
    engine.handleHTTPRequest(c)
    
    // 把 context 放回对象池
    engine.pool.Put(c)
}


使用时机

(1)handlesChain 入口

// 在 Engine.handleHTTPRequest 方法处理请求时,会通过 path 从 methodTree 中获取到对应的 handlers 链,然后将 handlers 注入到 Context.handlers 中,然后启动 Context.Next 方法开启 handlers 链的遍历调用流程.

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

(2)handlesChain 遍历调用

在这里插入图片描述


// 推进 handlers 链调用进度的方法正是 Context.Next. 可以看到其中以 Context.index 为索引,通过 for 循环依次调用 handlers 链中的 handler.

func (c *Context) Next() {
   c.index++ //再reset之后,index下表是-1
   for c.index < int8(len(c.handlers)) {
       c.handlers[c.index](c)
       c.index++
   }
}


// 由于 Context 本身会暴露于调用链路中,因此用户可以在某个 handler 中通过手动调用 Context.Next 的方式来打断当前 handler 的执行流程,提前进入下一个 handler 的处理中.

// 由于此时本质上是一个方法压栈调用的行为,因此在后置位 handlers 链全部处理完成后,最终会回到压栈前的位置,执行当前 handler 剩余部分的代码逻辑.

在这里插入图片描述

// 结合下面的代码示例来说,用户可以在某个 handler 中,于调用 Context.Next 方法的前后分别声明前处理逻辑和后处理逻辑,这里的“前”和“后”相对的是后置位的所有 handler 而言.

func myHandleFunc(c *gin.Context){
    // 前处理
    preHandle()  
    c.Next()
    // 后处理
    postHandle()
}
 

// 此外,用户可以在某个 handler 中通过调用 Context.Abort 方法实现 handlers 链路的提前熔断.

// 其实现原理是将 Context.index 设置为一个过载值 63,导致 Next 流程直接终止. 这是因为 handlers 链的长度必须小于 63,否则在注册时就会直接 panic. 因此在 Context.Next 方法中,一旦 index 被设为 63,则必然大于整条 handlers 链的长度,for 循环便会提前终止.

const abortIndex int8 = 63


func (c *Context) Abort() {
    c.index = abortIndex
}
 

// 此外,用户还可以通过 Context.IsAbort 方法检测当前 handlerChain 是出于正常调用,还是已经被熔断.

func (c *Context) IsAborted() bool {
    return c.index >= abortIndex
}
 

// 注册 handlers,倘若 handlers 链长度达到 63,则会 panic

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    // 断言 handlers 链长度必须小于 63
    assert1(finalSize < int(abortIndex), "too many handlers")
    // ...
}
 

(3)共享数据存取

在这里插入图片描述

// gin.Context 作为 handlers 链的上下文,还提供对外暴露的 Get 和 Set 接口向用户提供了共享数据的存取服务,相关操作都在读写锁的保护之下,能够保证并发安全.

 

type Context struct {
    // ...
    // 读写锁,保证并发安全
    mu sync.RWMutex


    // key value 对存储 map
    Keys map[string]any
}
 

func (c *Context) Get(key string) (value any, exists bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    value, exists = c.Keys[key]
    return
}
 

func (c *Context) Set(key string, value any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.Keys == nil {
        c.Keys = make(map[string]any)
    }


    c.Keys[key] = value
}

总结

  • gin 将 Engine 作为 http.Handler 的实现类进行注入,从而融入 Golang net/http 标准库的框架之内
    • gin 中基于 handler 链的方式实现中间件和处理函数的协调使用
    • gin 中基于压缩前缀树的方式作为路由树的数据结构,对应于 9 种 http 方法共有 9 棵树
    • gin 中基于 gin.Context 作为一次 http 请求贯穿整条 handler chain 的核心数据结构
    • gin.Context 是一种会被频繁创建销毁的资源对象,因此使用对象池 sync.Pool 进行缓存复用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1576112.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

零代码编程:用kimichat打造一个最简单的window程序

用kimichat可以非常方便的自动生成程序代码&#xff0c;有些小程序可能会频繁使用&#xff0c;如果每次都在vscode中执行就会很麻烦。常用的Python代码&#xff0c;可以直接做成一个window程序&#xff0c;点击就可以打开使用&#xff0c;方便很多。 首先&#xff0c;把kimich…

VGA显示器字符显示

1.原理 64*64256 2.1 Vga_pic.v module Vga_pic(input wire Vga_clk ,input wire sys_rst_n ,input wire [9:0] pix_x ,input wire [9:0] pix_y ,output reg [15:0] pix_data );parameter CHAR_B_H10d192,CHAR_B_V10d208;parameter CHAR_W10d256,CHAR_H10d64;paramet…

Linux从入门到精通 --- 4(上).快捷键、软件安装、systemctl、软链接、日期和时区、IP地址

文章目录 第四章(上)&#xff1a;4.1 快捷键4.1.1 ctrl c 强制停止4.1.2 ctrl d 退出4.1.3 history4.1.4 历史命令搜索4.1.5 光速移动快捷键4.1.6 清屏 4.2 软件安装4.2.1 yum4.2.2 apt 4.3 systemctl4.4 软链接4.4.1 ln 4.5 日期和时区4.5.1 date命令4.5.2 date进行日期加减…

阿里云服务器可以干嘛?阿里云服务器八大用途介绍

阿里云服务器可以干嘛&#xff1f;能干啥你还不知道么&#xff01;简单来讲可用来搭建网站、个人博客、企业官网、论坛、电子商务、AI、LLM大语言模型、测试环境等&#xff0c;阿里云百科aliyunbaike.com整理阿里云服务器的用途&#xff1a; 阿里云服务器活动 aliyunbaike.com…

怎么把学浪的视频保存到手机

越来越多的人在学浪app里面购买了课程并且想要下载下来&#xff0c;但是苦于没有方法或者工具&#xff0c;所以本文将教大家如何把学浪的视频保存到手机随时随地的观看&#xff0c;再也不用担心课程过期的问题。 本文将介绍工具来下载&#xff0c;因为下载方法太复杂&#xff…

Django检测到会话cookie中缺少HttpOnly属性手工复现

一、漏洞复现 会话cookie中缺少HttpOnly属性会导致攻击者可以通过程序(JS脚本等)获取到用户的cookie信息&#xff0c;造成用户cookie信息泄露&#xff0c;增加攻击者的跨站脚本攻击威胁。 第一步&#xff1a;复制URL&#xff1a;http://192.168.43.219在浏览器打开&#xff0c;…

Switch摇杆模块超好手感超小体积-适用于Arduino创客

Mini摇杆模块 1.模块照片 2.接线 摇杆模块的 G、V、X、Y 、SW分别连接 UNO 的G、V、A0、A1、D2引脚。 3.程序 /*rocker test- 摇杆测试This example code is in the public domain.Author : YFROBOT ZLWebsite : www.yfrobot.com.cnCreate Time: 2024 */#define XP…

自定义gitlog格式

git log命令非常强大而好用&#xff0c;在复杂系统的版本管理中扮演着重要的角色&#xff0c;但默认的git log命令显示出的东西实在太丑&#xff0c;不好好打扮一下根本没法见人&#xff0c;打扮好了用alias命令拍个照片&#xff0c;就正式出道了&#xff01; 在使用git查看lo…

CentOS7.9.2009安装elasticsearch7.11.1(单节点)

本文章使用CentOS7.9.2009服务器安装elasticsearch7.11.1软件 1.服务器信息 [root@elasticsearch ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [root@elasticsearch ~]# [root@elasticsearch ~]# cat /etc/hosts | grep elasticsearch 192.168.10.24…

消息队列MQ(面试题:为什么使用MQ)

一、什么是mq? MQ全称 Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信&#xff0c;解耦。 二、常见的mq产品 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq RabbitMQ: One broker …

34470A是德科技34470A数字万用表

181/2461/8938产品概述&#xff1a; Truevolt数字万用表&#xff08;34460A、34461A、34465A、34470A&#xff09;利用是德科技的新专利技术&#xff0c;使您能够快速获得见解、测量低功耗设备并保持校准的测量结果。Truevolt提供全方位的测量能力&#xff0c;具有更高的精度、…

Centos7源码方式安装Elasticsearch 7.10.2单机版

下载 任选一种方式下载 官网7.10.2版本下载地址&#xff1a; https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.2-linux-x86_64.tar.gz 网盘下载链接 链接&#xff1a;https://pan.baidu.com/s/1EJvUPGVOkosRO2PUaKibaA?pwdbnqi 提取码&#x…

mysqldump: Got error: 1049: Unknown database ‘root‘ when selecting the datab

1.问题描述 MySQL版本号&#xff1a;MySQL Server 8.3MySQL持久化到处数据库结构及数据 mysqldump: Got error: 1049: Unknown database root when selecting the datab2.问题解决 cmd 切换本地路径 cd /d D:\Program Files\MySQL\MySQL Server 8.3\bin执行数据库备份命令 …

【Linux系列】“dev-node1“ 运行的操作系统分析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

avro c++编译与使用

一、arvo介绍 Avro 是 Hadoop 中的一个子项目&#xff0c;也是一个数据序列化系统&#xff0c;其数据最终以二进制格式&#xff0c;采用行式存储的方式进行存储。 Avro提供了&#xff1a; 1)、丰富的数据结构。 2)、可压缩、快速的二进制数据格式。 3)、一个用来存储持久化数据…

飞书文档如何在不同账号间迁移

今天由于个人需要新建了一个飞书账号&#xff0c;遇到个需求就是需要把老帐号里面的文档迁移到新的账号里面。在网上搜了一通&#xff0c;发现关于此的内容似乎不多&#xff0c;只好自己动手解决&#xff0c;记录一下过程以便分享&#xff0c;主要有以下几个步骤。 1. 添加新账…

如何使用Chainguard保护您的容器部署

你不需要说服我&#xff0c;容器绝对是虚拟化革命中最棒的东西。我几乎每天都会使用各种类型的容器。 但是&#xff0c;只需一次模板化并到处部署的美妙之处伴随着一个成本&#xff1a;如果模板中的一个单独层包含安全漏洞怎么办&#xff1f;如果那里藏着一个漏洞&#xff0c;…

golang web 开发 —— gin 框架 (gorm 链接 mysql)

目录 1. 介绍 2. 环境 3. gin 3.1 gin提供的常见路由 3.2 gin的分组 main.go router.go 代码结构 3.3 gin 提供的Json方法 main.go route.go common.go user.go order.go 3.4 gin框架下如何获取传递来的参数 第一种是GET请求后面直接 /拼上传递的参数 第二种是…

pandas去重、删除重复数据之duplicated()

pandas去重、删除重复数据之duplicated 1.pandas中重复索引问题2.pandas删除重复数据行3.drop_duplicates()函数的语法4.案例&#xff1a;pandas数据处理——取出重复数据 1.pandas中重复索引问题 df df[~df.index.duplicated()]2.pandas删除重复数据行 # 首先导入常用的两个…

AURORA64B66B IP核使用

文章目录 前言一、IP核配置二、设计框图三、上板效果总结 前言 前面我们基于GT 64B66B设计了自定义PHY层&#xff0c;并且也介绍过了基于AURORA8B18B IP核的使用&#xff0c;AURORA8B18B IP核的使用可以说是与AURORA8B18B IP核完全一致&#xff0c;可参考前文&#xff1a;http…