Gin框架 源码解析

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.kler.cn/a/274841.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图神经网络实战(5)——常用图数据集

图神经网络实战&#xff08;5&#xff09;——常用图数据集 0. 前言0. 图数据集介绍2. Cora 数据集3. Facebook Page-Page 数据集小结系列链接 0. 前言 图数据集往往比单纯的连接集合更丰富&#xff0c;节点和边也可以具有表示分数、颜色、单词等的特征。在输入数据中包含这些…

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

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

【web世界探险家】HTML5 探索与实践

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 |《MySQL探索之旅》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &…

【SpringBoot3+MyBatis-Plus】头条新闻项目实现CRUD登录注册

文章目录 一、头条案例介绍二、技术栈介绍三、前端搭建四、基于SpringBoot搭建项目基础架构4.1 数据库脚本执行4.2 搭建SprintBoot工程4.2.1 导入依赖:4.2.2 编写配置4.2.3 工具类准备 4.3 MybatisX逆向工程 五、后台功能开发5.1 用户模块开发5.1.1 jwt 和 token 介绍5.1.2 jwt…

webpack5零基础入门-12搭建开发服务器

1.目的 每次写完代码都需要手动输入指令才能编译代码&#xff0c;太麻烦了&#xff0c;我们希望一切自动化 2.安装相关包 npm install --save-dev webpack-dev-server 3.添加配置 在webpack.config.js中添加devServer相关配置 /**开发服务器 */devServer: {host: localhos…

Docker 从0安装 nacos集群

前提条件 Docker支持一下的CentOs版本 Centos7(64-bit)&#xff0c;系统内核版本为 3.10 以上Centos6.5(64-bit) 或者更高版本&#xff0c;系统内核版本为 2.6.32-431 或者更高版本 安装步骤 使用 yum 安装&#xff08;CentOS 7下&#xff09; 通过 uname -r 命令查看你当…

Linux文件 profile、bashrc、bash_profile区别

Linux系统中&#xff0c;有三种文件 出现的非常频繁&#xff0c;那就是 profile、bash_profile、bashrc 文件。 1、profile 作用 profile&#xff0c;路径&#xff1a;/etc/profile&#xff0c;用于设置系统级的环境变量和启动程序&#xff0c;在这个文件下配置会对所有用户…

vivado 物理优化约束、交互式物理优化

物理优化约束 Vivado Design Suite在物理优化过程中尊重DONT_TOUCH特性。它不在具有这些属性的网络或小区上执行物理优化。要加快网络选择过程中&#xff0c;具有DONT_TOUCH属性的网络经过预过滤&#xff0c;不被考虑用于物理优化。此外&#xff0c;还遵守Pblock分配&#xff…

嵌入式学习41-数据结构2

今天学习了链表的增删改查 &#xff08;暂定&#xff01;&#xff01;后续再补内容&#xff09; 高内聚 &#xff1a;一个函数只实现一个功能 …

Java学习笔记:异常处理

Java学习笔记&#xff1a;异常处理 什么是异常异常体系结构&#xff1a;Error、Exception自定义异常 ​ **2024/3/19** 什么是异常 异常体系结构&#xff1a;Error、Exception 自定义异常

UE5 GAS开发p30 创建UI HUD 血条

新建AuraUserWidget,AuraWidgetController,AuraHUD 首先设置AuraWidgetController.h // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "AbilitySystemComponent.h" #in…

风丘电动汽车高压测试方案 助您高效应对车辆试验难题

针对电动汽车道路试验的要求&#xff0c;风丘科技携手德国IPETRONIK共同推出了电动汽车高压测试方案。电动汽车测试通常有两种测量手段&#xff1a;第一种是测量模拟量信号&#xff0c;包括电压、电流、温度和高压&#xff1b;第二种是使用数据记录仪或CAN卡从车辆总线读取数据…

JS+CSS3点击粒子烟花动画js特效

JSCSS3点击粒子烟花动画js特效 JSCSS3点击粒子烟花动画js特效

C++:类和对象(上篇)

目录&#xff1a; 一&#xff1a;面向对象和过程的介绍 二&#xff1a;类的引入 三&#xff1a;类的定义 四&#xff1a;类的访问限定符以及封装 五&#xff1a;类的作用域 六&#xff1a;类的实例化 七&#xff1a;类对象大小的计算 八&#xff1a;类成员函数的this指…

一次完整的 HTTP 请求所经历的步骤

1&#xff1a; DNS 解析(通过访问的域名找出其 IP 地址&#xff0c;递归搜索)。 2&#xff1a; HTTP 请求&#xff0c;当输入一个请求时&#xff0c;建立一个 Socket 连接发起 TCP的 3 次握手。如果是 HTTPS 请求&#xff0c;会略微有不同。 3&#xff1a; 客户端向服务器发…

智慧城市的发展趋势与挑战:未来展望

随着信息技术的飞速发展&#xff0c;智慧城市已成为现代城市发展的重要方向。智慧城市通过集成应用先进的信息通信技术&#xff0c;实现城市管理、服务、运行的智能化&#xff0c;为城市的可持续发展注入了新的活力。然而&#xff0c;在智慧城市的发展过程中&#xff0c;也面临…

Android Studio实现内容丰富的安卓外卖平台

获取源码请点击文章末尾QQ名片联系&#xff0c;源码不免费&#xff0c;尊重创作&#xff0c;尊重劳动 项目编号122 1.开发环境android stuido jdk1.8 eclipse mysql tomcat 2.功能介绍 安卓端&#xff1a; 1.注册登录 2.查看公告 3.查看外卖分类 4.购物车&#xff0c; 5.个人中…

当人工智能遇上“理解偏差”:一场关于天气的宇宙之旅

在这个由数据和算法驱动的时代&#xff0c;人工智能(AI)无疑成为了我们日常生活中的重要组成部分。从智能助手到个性化推荐系统&#xff0c;AI的应用范围正在不断扩大&#xff0c;旨在为我们提供更加便捷、高效的服务。然而&#xff0c;尽管AI技术取得了显著进展&#xff0c;仍…

docker入门(五)—— 小练习,docker安装nginx、elasticsearch

练习 docker 安装 nginx # 搜素镜像 [rootiZbp15293q8kgzhur7n6kvZ home]# docker search nginx NAME DESCRIPTION STARS OFFICIAL nginx …

python大学生健身爱好者交流网站flask-django-nodejs-php

任何系统都要遵循系统设计的基本流程&#xff0c;本系统也不例外&#xff0c;同样需要经过市场调研&#xff0c;需求分析&#xff0c;概要设计&#xff0c;详细设计&#xff0c;编码&#xff0c;测试这些步骤&#xff0c;基于python技术、django/flask框架、B/S机构、Mysql数据…
最新文章