Gin框架 源码解析

news2024/11/27 10:22:41

https://zhuanlan.zhihu.com/p/136253346
https://www.cnblogs.com/randysun/category/2071204.html 这个博客里其他go的内容也讲的很好

启动

因为 gin 的安装教程已经到处都有了,所以这里省略如何安装, 建议直接去 github 官方地址的 README 中浏览安装步骤,顺便了解 gin 框架的功能。https://github.com/gin-gonic/gin

最简单的代码

package main
​
import "github.com/gin-gonic/gin"func main() {
    r := gin.Default()
    
    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}


上面就是使用了 gin 框架的最简单的 demo,接下来通过这个 demo 去一步步阅读分析 gin 是如何启动的。

gin.Run()

这里是服务器启动的地方,进去看看这个函数有什么奥秘:

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()
​
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

除去第一行和第四行打印,是不是有点像 go 原生的 http 库?来看看 http 库的 web 服务简单 demo

package main
​
import (
    "log"
    "net/http"
)func main() {
    http.HandleFunc("/", indexHandler)
​
    log.Fatal(http.ListenAndServe(":8080", nil))
}

可以看到 gin 框架启动的时候,也是基于 http.ListenAndServe(addr string, handler Handler) 这个方法的,与原生的 http 库的区别就是,ListenAndServe(addr string, handler Handler) 这个方法的第二个参数,传入的是 engine,我们继续来看看 ListenAndServe 方法的具体代码:

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

server.ListenAndServe() 已经是服务监听并启动的方法,所以关键点在 Handler 这里:

// A Handler responds to an HTTP request.
// ...
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这是 http 包里的源码注释,其实写得很详细了,注释很多,我直接省略了,感兴趣的可以去阅读一下,不出意外,这个 Handler 是一个接口,从 gin.engine 的传入已经可以看出来了吧?注释的意思是这个接口是处理程序响应HTTP请求,可以理解为,实现了这个接口,就可以接收所有的请求并进行处理。

因此 gin 框架的入口就是从这里开始,gin engine 实现了 ServeHTTP,然后接管所有的请求走 gin 框架的处理方式。细心的读者应该能发现,原生的 http web 服务传入了 nil,事实上是 http 库也有一个默认的请求处理器,感兴趣的读者可以去仔细阅读研究一下 go 官方团队的请求处理器实现哦~

那我们继续来看下 gin 框架是怎么实现这个 ServeHTTP 方法的:

// gin.go
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // 从池里获取资源处理请求
    c.writermem.reset(w) // 重置资源的 ResponseWriter
    c.Request = req // 赋值请求给 context
    c.reset()
​
    engine.handleHTTPRequest(c) // 将资源信息给到 handler 进一步处理
​
    engine.pool.Put(c) // 请求处理结束,将资源放回池子
}

可以看到实现并不难,代码的含义我已经写上了注释,这里的重点是 Context,但不是这一篇文章的重点,我放在这一系列的后面进行讲解。

我们只需要知道,gin 也是实现了 Handler 接口,所以可以将请求按 gin 的处理方式进行处理,也就是我们可以使用 gin 来做 web 服务框架的起点。

gin.Default()

接下来进去 Default函数去看具体实现,代码如下:

// gin.go
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault() // 忽略
    engine := New()
    engine.Use(Logger(), Recovery()) // 暂时忽略
    return engine
}

其实单单看函数名,已经知道这是构造默认的 gin engine,可以看到 engine 是通过 New() 方法得到的,我们选忽略第一行和第三行。

// gin.go
func New() *Engine {
    debugPrintWARNINGNew() // 忽略
    engine := &Engine{
        RouterGroup: RouterGroup{ // 路由组,后面再分析
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine, // bool,是否为默认处理 engine
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine  // 重点,将 engine 重新赋值给路由组对象中的 engine
    engine.pool.New = func() interface{} { // 重点,资源池
        return engine.allocateContext()
    }
    return engine
}

到这里,我们不用看 engine 结构的具体定义,也已经看出来比较多的信息了,主要用路由组和资源池,这两个都可以分别展开写一篇文章,由于篇幅有限,这里就先不介绍了。

值得注意的是这里面有两个地方很巧妙:

engine.RouterGroup.engine = engine

很明显,路由组 RouterGroup 中还有个 engine 的指针对象,为什么要这么设计呢?读者们可以思考一下。

engine.pool.New = func() interface{} { // 对象池
    return engine.allocateContext()
}

看下 engine.allocateContext()方法:

func (engine *Engine) allocateContext() *Context {
    return &Context{engine: engine}
}

可以看到 engine 中包含了 pool 对象池,这个对象池是对 gin.Context 的重用,进一步减少开销,关于 sync.pool 对象池我就不在这里细说了, 后续再更新关于 sync.pool 的文章。

engine
最后再来看看 engine 的结构:

type Engine struct {
    // 路由组
    RouterGroup
​
    // 如果true,当前路由匹配失败但将路径最后的 / 去掉时匹配成功时自动匹配后者
    // 比如:请求是 /foo/ 但没有命中,而存在 /foo,
    // 对get method请求,客户端会被301重定向到 /foo
    // 对于其他method请求,客户端会被307重定向到 /foo
    RedirectTrailingSlash bool// 如果true,在没有处理者被注册来处理当前请求时router将尝试修复当前请求路径
    // 逻辑为:
    // - 移除前面的 ../ 或者 //
    // - 对新的路径进行大小写不敏感的查询
    // 如果找到了处理者,请求会被301或307重定向
    // 比如: /FOO 和 /..//FOO 会被重定向到 /foo
    // RedirectTrailingSlash 参数和这个参数独立
    RedirectFixedPath bool// 如果true,当路由没有被命中时,去检查是否有其他method命中
    //  如果命中,响应405 (Method Not Allowed)
    //  如果没有命中,请求将由 NotFound handler 来处理
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool// #726 #755 If enabled, it will thrust some headers starting with
    // 'X-AppEngine...' for better integration with that PaaS.
    AppEngine bool// 如果true, url.RawPath 会被用来查找参数
    UseRawPath bool// 如果true, path value 会被保留
    // 如果 UseRawPath是false(默认),UnescapePathValues为true
    // url.Path会被保留并使用
    UnescapePathValues bool// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
    // method call.
    MaxMultipartMemory int64
​
    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    pool             sync.Pool
    
    //每个http method对应一棵树
    trees            methodTrees
}

上面提到过 ServeHTTP 这个方法,其中engine.handleHTTPRequest©这行代码就是具体的处理操作。

可以看到 engine 的结构中有这么一个字段 RedirectTrailingSlash ,在 Default() 初始化方法中为 true,我对此比较感兴趣,大家也可以根据注释的意思来测试一下,最终会走到下面的代码中:

func (engine *Engine) handleHTTPRequest(c *Context) {
    ……
    if httpMethod != "CONNECT" && rPath != "/" {
        if value.tsr && engine.RedirectTrailingSlash {
            // 这里就是尝试纠正请求路径的函数
            redirectTrailingSlash(c)
            return
        }
        if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
        }
    }
    ……
}

总结一下
在这里插入图片描述

上图就是本文的核心了,可以结合图来理解一下 gin 启动的过程及设计,下一篇会将 gin 的路由,敬请期待~

本系列 “拆轮子系列:gin 框架” 的第一篇就到这里了,这么通读下来,发现 gin 框架的设计和实现真的太棒了,简洁清晰,又不失巧妙,很适合大家也去阅读学习一下,墙裂推荐!!!

路由 gin.RouterGroup结构体及其方法

用法

还是老样子,先从使用方式开始:

func main() {
    r := gin.Default()
​
    r.GET("/hello", func(context *gin.Context) {
        fmt.Fprint(context.Writer, "hello world")
    })
​
    r.POST("/somePost", func(context *gin.Context) {
        context.String(http.StatusOK, "some post")
    })
​
    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

平时开发中,用得比较多的就是 Get 和 Post 的方法,上面简单的写了个 demo,注册了两个路由及处理器,接下来跟着我一起一探究竟

注册路由

从官方文档和其他大牛的文章中可以知道,gin的路由是借鉴了 httprouter 实现的路由算法,所以得知 gin 的路由算法是基于前缀树这个数据结构的。

从 Get 方法进去看源码:

r.GET("/hello", func(context *gin.Context) {
    fmt.Fprint(context.Writer, "hello world")
})

会来到 routergroup.go 的 Get 函数,可以发现方法的承载者已经是 *RouterGroup:

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("GET", relativePath, handlers)
}
从注释中我们可以看到 `GET is a shortcut for router.Handle("GET", path, handle)`

也就是说 GET 方法的注册也可以等价于:

    helloHandler := func(context *gin.Context) {
            fmt.Fprint(context.Writer, "hello world")
        }
​
    r.Handle("GET", "/hello", helloHandler)

再来看一下 Handle 方法的具体实现:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
    if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
        panic("http method " + httpMethod + " is not valid")
    }
    return group.handle(httpMethod, relativePath, handlers)
}

不难发现,无论是 r.GET 还是 r.Handle 最终都是指向了 group.handle:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 计算绝对路径,这是因为可能会有路由组会在外层包裹的原因
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 联合路由组的 handler 和新注册的 handler
    handlers = group.combineHandlers(handlers)
    // 注册路由的真正入口
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    // 返回 IRouter 接口对象,这个放在路由组进行分析
    return group.returnObj()
}

接下来又回到了 gin.go ,可以看到上面的注册入口是通过group.engine 调用的,大家不用看 routerGroup 的结构也大致猜出来了吧,其实 engine 才是真正的路由树 router,而 gin 为了实现路由组的功能,所以在外面又包了一层 routerGroup,实现路由分组,路由路径组合隔离的功能。

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    // 基础校验
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")
    
    debugPrintRoute(method, path, handlers)
    // 每个httpMethod都拥有自己的一颗树
    root := engine.trees.get(method)
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    // 在路由树中添加路径及请求处理handler
    root.addRoute(path, handlers)
}

在这里插入图片描述

以上就是注册路由的过程,整体流程其实挺清晰的。

路由树

终于来到了关键的实现路由树的地方tree.go:

先来看看 tree 的结构:

type methodTree struct {
    method string
    root   *node
}type methodTrees []methodTree

在这里插入图片描述

上面的 engine.trees.get(method) 就是遍历这个以 httpMethod 分隔的数组:

func (trees methodTrees) get(method string) *node {
    for _, tree := range trees {
        if tree.method == method {
            return tree.root
        }
    }
    return nil
}


关键在于 node:

type node struct {
    path      string // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径)
    indices   string // 所有孩子节点的path[0]组成的字符串
    children  []*node // 孩子节点
    handlers  HandlersChain // 当前节点的处理函数(包括中间件)
    priority  uint32 // 当前节点及子孙节点的实际路由数量
    nType     nodeType // 节点类型
    maxParams uint8 // 子孙节点的最大参数数量
    wildChild bool // 孩子节点是否有通配符(wildcard)
    fullPath  string // 路由全路径
}

nType 有这几个值:

const (
    static nodeType = iota // 普通节点,默认
    root // 根节点
    param // 参数路由,比如 /user/:id
    catchAll // 匹配所有内容的路由,比如 /article/*key
)

下面的 addRoute 方法就是对这棵前缀树的构建过程,实际上就是不断寻找最长前缀的过程。

func (n *node) addRoute(path string, handlers HandlersChain) {
    ……
    // non-empty tree
    if len(n.path) > 0 || len(n.children) > 0 {
    walk:
            ……
​
            // Make new node a child of this node
            if i < len(path) {
                ……
                c := path[0]
                // 一系列的判断与校验
                ……
                // Otherwise insert it
                if c != ':' && c != '*' {
                    // []byte for proper unicode char conversion, see #65
                    n.indices += string([]byte{c})
                    child := &node{
                        maxParams: numParams,
                        fullPath:  fullPath,
                    }
                    n.children = append(n.children, child)
                    n.incrementChildPrio(len(n.indices) - 1)
                    n = child
                }
                // 经过重重困难,终于可以摇到号了
                n.insertChild(numParams, path, fullPath, handlers)
                return} else if i == len(path) { // Make node a (in-path) leaf
                // 路由重复注册
                if n.handlers != nil {
                    panic("handlers are already registered for path '" + fullPath + "'")
                }
                n.handlers = handlers
            }
            return
        }
    } else { // Empty tree
        // 空树则直接插入新节点
        n.insertChild(numParams, path, fullPath, handlers)
        n.nType = root
    }
}

最后画一下 gin 构建前缀树的示意图:

r.GET(“/”, func(context *gin.Context) {})
r.GET(“/test”, func(context *gin.Context) {})
r.GET(“/te/n”, func(context *gin.Context) {})
r.GET(“/pass”, func(context *gin.Context) {})
r.GET(“/part/:id”, func(context *gin.Context) {})
r.GET(“/part/:id/pen”, func(context *gin.Context) {})

在这里插入图片描述

动态路由

在画前缀树的时候,写到一个了路由 /part/:id,这里的 :id 就是动态路由了,可以根据路由中指定的参数来解析 url 中对应动态路由里的参数值。

其实在说到 node 的数据结构的时候,已经提到了 nType、maxParams、wildChild这三个字段与动态路由的设计实现有关的,下面就是关于路由注册时如果是动态路由时的处理:

// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
        ……
    
        if c == ':' { // param
            // 在通配符开头拆分路径
            if i > 0 {
                n.path = path[offset:i]
                offset = i
            }
​
            child := &node{
                nType:     param,
                maxParams: numParams,
                fullPath:  fullPath,
            }
            n.children = []*node{child}
            // 如果孩子节点是参数路由,就会将本节点wildChild设置为true
            n.wildChild = true
            n = child
            n.priority++
            numParams--// 如果路径没有以通配符结尾,则将有另一个以"/" 开头的非通配符子路径
            // 可以理解为后面还有节点
            if end < max {
                n.path = path[offset:end]
                offset = end
​
                child := &node{
                    maxParams: numParams,
                    priority:  1,
                    fullPath:  fullPath,
                }
                n.children = []*node{child}
                n = child
            }} else { // catchAll
            ……
​
            n.path = path[offset:i]// 匹配所有内容的通配符 如 /*key
            
            // first node: catchAll node with empty path
            child := &node{
                wildChild: true,
                nType:     catchAll,
                maxParams: 1,
                fullPath:  fullPath,
            }
            n.children = []*node{child}
            n.indices = string(path[i])
            // 在这里将 node 进行赋值了
            n = child
            n.priority++// second node: node holding the variable
            child = &node{
                path:      path[i:],
                nType:     catchAll,
                maxParams: 1,
                handlers:  handlers,
                priority:  1,
                fullPath:  fullPath,
            }
            n.children = []*node{child}return
        }
    }// insert remaining path part and handle to the leaf
    n.path = path[offset:]
    n.handlers = handlers
    n.fullPath = fullPath
}


我们知道 gin 框架中对于动态路由参数接收时是用 context.Param(key string) 的,下面跟着一个简单的 demo 来做

helloHandler := func(context *gin.Context) {
        name := context.Param("name")
        fmt.Fprint(context.Writer, name)
    }
​
r.Handle("GET", "/hello/:name", helloHandler)
来看下 Param 写了啥:

// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string {
    return c.Params.ByName(key)
}

看注释,其实写得已经很明白了,这个函数会返回动态路由中关于参数在请求 url 里的值,再往深处走,Params 和 ByName 其实来自 tree.go:

// context.go
type Context struct {
    ……
​
    Params   Params
    ……
}// tree.go
type Param struct {
    Key   string
    Value string
}// Params 是有个有序的 Param 切片,路由中的第一个参数会对应切片的第一个索引
type Params []Param
​
// 遍历 Params 获取值
func (ps Params) Get(name string) (string, bool) {
    for _, entry := range ps {
        if entry.Key == name {
            return entry.Value, true
        }
    }
    return "", false
}// 封装了一下,调用上面的 Get 方法
func (ps Params) ByName(name string) (va string) {
    va, _ = ps.Get(name)
    return
}
获取参数 key 的地方找到了,那从路由里拆解并设置 Params 的地方呢?

// tree.gotype nodeValue struct {
    handlers HandlersChain
    params   Params
    tsr      bool
    fullPath string
}// getValue 返回的 nodeValue 的结构,里面包含处理好的 Params
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
    value.params = po
walk: // Outer loop for walking the tree
    for {
        if len(path) > len(n.path) {
            if path[:len(n.path)] == n.path {
                path = path[len(n.path):]
                // 如果这个节点没有通配符,就进行往孩子节点遍历
                if !n.wildChild {
                    c := path[0]
                    for i := 0; i < len(n.indices); i++ {
                        if c == n.indices[i] {
                            n = n.children[i]
                            continue walk
                        }
                    }// 如果没找到有通配符标识的节点,直接重定向到该 url
                    value.tsr = path == "/" && n.handlers != nil
                    return
                }// handle wildcard child
                n = n.children[0]
                switch n.nType {
                    //可以看到这里是用 nType 来判断的
                case param:
                    // find param end (either '/' or path end)
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }// 遍历 url 获取参数对应的值
                    
                    // save param value
                    if cap(value.params) < int(n.maxParams) {
                        value.params = make(Params, 0, n.maxParams)
                    }
                    i := len(value.params)
                    value.params = value.params[:i+1] // expand slice within preallocated capacity
                    value.params[i].Key = n.path[1:] // 除去 ":",如 :id -> id
                    val := path[:end]
                    // url 编码解析以及 params 赋值
                    if unescape {
                        var err error
                        if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
                            value.params[i].Value = val // fallback, in case of error
                        }
                    } else {
                        value.params[i].Value = val
                    }
                ……
        }
    }
}


讲到这里就已经对路由注册和动态路由的实现流程和原理分析得差不多了,画一个核心流程图总结一下:
在这里插入图片描述

路由组

gin 用 RouterGroup 路由组包住了路由实现了路由分组功能。之前说到 engine 的时候说到 engine 的结构中是组合了 RouterGroup 的,而 RouterGroup 中其实也包含了 engine:

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

这样的做法让 engine 直接拥有了管理路由的能力,也就是 engine.GET(xxx) 可以直接注册路由的来由。而 RouterGroup 中包含了 engine 的指针,这样实现了 engine 的单例,这个也是比较巧妙的做法之一。

不仅如此,RouterGroup 实现了 IRouter 接口,接口中的方法都是通过调用 engine.addRoute()` 将handler链接到路由树中:

var _ IRouter = &RouterGroup{}type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}type IRoutes interface {
    Use(...HandlerFunc) IRoutes
​
    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes
​
    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

路由组的功能显而易见,就是让路由分组管理,在组内的路由的前缀都统一加上组路由的路径,看下 demo:

router := gin.Default()
​
v1 := router.Group("/v1")
{
    v1.POST("/hello", helloworld) // /v1/hello
    v1.POST("/hello2", helloworld2) // /v1/hello2
}

包住路由并在注册路由时进行拼接的地方是在注册路由的函数中:

// routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 拼接获取绝对路径
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 合并路由处理器集合
    handlers = group.combineHandlers(handlers)
    ……
}

中间件

gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()

一、中间件的注册

gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的 r:= gin.Default()的Default函数开始看,它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())  // 默认注册的两个中间件
	return engine
}

继续往下查看一下Use()函数的代码:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)  // 实际上还是调用的RouterGroup的Use函数
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

从下方的代码可以看出,注册中间件其实就是将中间件函数追加到group.Handlers中:

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

而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起:

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()
}

其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的。

const abortIndex int8 = math.MaxInt8 / 2

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {  // 这里有一个最大限制
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain,而它本质上就是一个由HandlerFunc组成的切片:

type HandlersChain []HandlerFunc

二、中间件的执行

我们在上面路由匹配的时候见过如下逻辑:

value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
  c.handlers = value.handlers
  c.Params = value.params
  c.fullPath = value.fullPath
  c.Next()  // 执行函数链条
  c.writermem.WriteHeaderNow()
  return
}

其中c.Next()就是很关键的一步,它的代码很简单:

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

从上面的代码可以看到,这里通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)。
在这里插入图片描述

我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3),
在这里插入图片描述
或者通过调用c.Abort()中断整个调用链条,从当前函数返回。

func (c *Context) Abort() {
	c.index = abortIndex  // 直接将索引置为最大限制值,从而退出循环
}

三、c.Set()/c.Get()

c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根绳子,将该次请求相关的所有的函数都串起来了。
在这里插入图片描述

四、总结

gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。
借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。

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

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

相关文章

QT 驾校系统界面布局编写

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);this->resize(ui->label_img->width(),ui->label_img->height());//图片自适应窗口大小ui->label_img->setScaledContents(true);//图片置…

sora笔记(三):diffusion transformer模型的发展历程

:::info &#x1f4a1; 在sora笔记&#xff08;一&#xff09;&#xff1a;sora前世今生与技术梗概 一文中介绍了目前未开源的sora模型可能涉及到的技术点&#xff0c;包括介绍了Vision Transformer&#xff0c;作为transformer正式用于图像的一种范式&#xff0c;为本文中将提…

慧海科创再探潮间带|全面调研推动梭子蟹产业进步

浙江的海岸线延绵,孕育了丰富的海洋生物多样性。在这样的背景下,慧海科创团队沿着宁波至舟山的潮间带开展了全面的调研活动。2024年3月15日,浙江海洋大学、宁波大学、上海理工大学的梭子蟹智能捆扎实践团队,深入海岸一线,与当地养殖户交流产业发展中的痛点难点,共同探讨梭子蟹产…

Java 多线程线程池分析

[1]前言 关于Java多线程的知识&#xff0c;看了很多博客书籍&#xff0c;对理论还是比较了解的。但是&#xff0c;最近写一个很简单的使用线程池对列表中任务进行处理&#xff0c;然后返回结果列表的功能&#xff0c;发现理论和实际操作还是有相当大的差距。 首先贴出一个很简…

适用于 Windows 2024 的 7 个最佳免费分区恢复软件分享

无法确定 2024 年 Windows 上最好的免费分区恢复软件是什么&#xff1f;那么&#xff0c;我们可以提供帮助&#xff01;我们测试了目前市场上可用的几种硬盘分区恢复软件 - 包括免费和付费版本。您现在所需要的只是 - 只需浏览列表并选择适合您要求的一项即可。继续阅读&#x…

ARM Cortex-R82处理器在压缩SSD场景的应用

ScaleFlux公司宣布在其下一代企业级SSD控制器产品线中采用Arm公司的Cortex-R82处理器。这一决策旨在应对企业环境中对高带宽存储解决方案日益增长的需求&#xff0c;并通过提升数据传输速度和效率来满足市场期待。 Arm Cortex-R82处理器是Arm公司迄今为止性能最强的实时处理器…

107 在携带请求体的情况下, hutool 将 get 请求转换为了 post 请求

前言 本问题主要是来自于同事 情况大致如下, 同样的代码 一个是测试用例, 一个是生产环境的应用, 访问同一个第三方服务, 参数什么的完全一致 但是 出现的问题就是 测试用例能够拿到正确的对方的响应, 但是 生产环境的应用 却是拿到的对方的报错 然后 我开始以为是 是否…

通过rmi实现远程rpc(可以认为java自带Dubbo RPC)

背景&#xff1a; 发现公司几个运行10年的游戏&#xff0c;用的竟然是rmi&#xff0c;而我只听说过dubbo 和 基于netty的rpc&#xff0c;于是就补充了下rmi。 其次&#xff0c;是最近对于跨服的思考&#xff0c;如何避免回调也需要用同步写法&#xff0c;rmi比较适合。 1)api…

算法笔记p251队列循环队列

目录 队列循环队列循环队列的定义初始化判空判满入队出队获取队列内元素的个数取队首元素取队尾元素 队列 队列是一种先进先出的数据结构&#xff0c;总是从队尾加入元素&#xff0c;从队首移除元素&#xff0c;满足先进先出的原则。队列的常用操作包括获取队列内元素的个数&a…

2023年蓝桥杯模拟省赛——列名

目录 题目链接&#xff1a;2.列名 - 蓝桥云课 (lanqiao.cn) 思路 高级思路&#xff1a;进制转换 难点一 难点二 难点三 总结 题目链接&#xff1a;2.列名 - 蓝桥云课 (lanqiao.cn) 思路 先来看我的暴力的思路吧 主要有以下步骤&#xff1a; 初始化一个长度为3的数组res用…

图像分割之k-means聚类分割

文章目录 k-means聚类原理k-means基本思路聚类效果评估k-means聚类算法流程MATLAB实现测试结果参考文献 k-means聚类原理 k-means聚类是一种无监督学习的聚类算法&#xff0c;它的目的是将数据集中的样本划分成若干个类别&#xff0c;使得同一类别内的样本相似度高&#xff0c…

Java:类和对象

目录 1.面对对象的初步认识1.1 什么是面向对象&#xff1f;&#xff08;Java当中一切皆为对象&#xff09;1.2 面对对象与面对过程 2.类的定义和使用2.1简单认识类2.2 类的定义格式 3.类的实例化3.1 什么是实例化3.2类和对象的说明 4.this引用4.1为什么要使用this引用4.2 什么是…

集合系列(十) -Set接口详解

一、摘要 关于 Set 接口&#xff0c;在实际开发中&#xff0c;其实很少用到&#xff0c;但是如果你出去面试&#xff0c;它可能依然是一个绕不开的话题。 言归正传&#xff0c;废话咱们也不多说了&#xff0c;相信使用过 Set 集合类的朋友都知道&#xff0c;Set集合的特点主要有…

重磅:Python 迎来多线程重大更新 no-GIL

“在 Python 中&#xff0c;GIL 将不复存在。对 AI 生态系统来说是巨大的胜利。”PyTorch 核心维护者 Dmytro Dzhulgakov 感慨地说道。 GIL 是什么&#xff1f;GIL 的全称是 Global Interpreter Lock&#xff08;全局解释器锁&#xff09;&#xff0c;这不仅是 Python 的特性…

计算机毕业设计-基于python的旅游信息爬取以及数据分析

概要 随着计算机网络技术的发展&#xff0c;近年来&#xff0c;新的编程语言层出不穷&#xff0c;python语言就是近些年来最为火爆的一门语言&#xff0c;python语言&#xff0c;相对于其他高级语言而言&#xff0c;python有着更加便捷实用的模块以及库&#xff0c;具有语法简单…

彻底学会系列:一、机器学习之梯度下降(2)

1 梯度具体是怎么下降的&#xff1f; ∂ J ( θ ) ∂ θ \frac{\partial J (\theta )}{\partial \theta} ∂θ∂J(θ)​&#xff08;损失函数&#xff1a;用来衡量模型预测值与真实值之间差异的函数&#xff09; 对损失函数求导&#xff0c;与学习率相乘&#xff0c;按梯度反方…

mabatis 下

mybatis 原生的API&注解的方式MyBatis-原生的API调用快速入门需求快速入门代码实现 MyBatis-注解的方式操作快速入门需求快速入门代码实现注意事项和说明 mybatis-config.xml配置文件详解说明properties属性settings全局参数定义typeAliases别名处理器typeHandlers类型处理…

长安链团队论文入选国际顶会Usenix Security 2024

零知识证明是区块链扩容和隐私保护的关键前沿技术&#xff0c;其天然具备完备性、可靠性和零知识性的特点&#xff0c;是提升区块链交易吞吐量与可扩展性、在验证用户身份的同时保护用户数据隐私&#xff0c;实现复杂计算不可或缺的关键技术。基于零知识证明技术实现高兼容性、…

C++ 组合 委托 继承 组合使用

关于组合和委托看C中的组合&#xff0c;委托和继承 - 知乎 (zhihu.com) 继承和组合关系下的构造和析构 ​ 还有一种情况 ​ 构造函数由内到外&#xff0c;析构由外到内。 委托和继承关系组合 设计模式-观察者模式&#xff08;Observer&#xff09; ​ 如下图左边&#x…

稀碎从零算法笔记Day22-LeetCode:

题型&#xff1a;链表 链接&#xff1a;2. 两数相加 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;Leet 题目描述 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 …