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()方法我们能够在不同的中间件函数中传递数据。

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

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

相关文章

【数据库基础增删改查】条件查询、分页查询

系列文章目录 &#x1f308;座右铭&#x1f308;&#xff1a;人的一生这么长、你凭什么用短短的几年去衡量自己的一生&#xff01; &#x1f495;个人主页:清灵白羽 漾情天殇_计算机底层原理,深度解析C,自顶向下看Java-CSDN博客 ❤️相关文章❤️&#xff1a;清灵白羽 漾情天…

AI浸入社交领域,泛娱乐APP如何抓住新风口?

2023年是大模型技术蓬勃发展的一年&#xff0c;自ChatGPT以惊艳姿态亮相以来&#xff0c;同年年底多模态大模型技术在国内及全球范围内的全面爆发&#xff0c;即模型能够理解并生成包括文本、图像、视频、音频等多种类型的内容。例如&#xff0c;基于大模型的文本到图像生成工具…

Samtec科普 | 一文了解患者护理应用连接器

【摘要/前言】 通过医疗专业人士为患者提供护理的种种需求&#xff0c;已经不限于手术室与医院的各种安全状况。当今许多患者的护理都是在其他环境进行&#xff0c;例如医生办公室、健康中心&#xff0c;还有越来越普遍的住家。尤其是需要长期看护的患者&#xff0c;所需的科技…

Mysql数据库概念与安装

目录 一、数据库概述 1、数据库的基本概念 2、数据库管理系统&#xff08;DBMS&#xff09; 2.1 数据库管理系统概念 2.2 数据库管理系统工作模式 3、数据库系统&#xff08;DBS&#xff09; 3.1 数据库系统概念 3.2 数据库系统发展史 4、关系型数据库与非关系型数据库…

机器学习——终身学习

终身学习 AI不断学习新的任务&#xff0c;最终进化成天网控制人类终身学习&#xff08;LLL&#xff09;&#xff0c;持续学习&#xff0c;永不停止的学习&#xff0c;增量学习 用线上收集的资料不断的训练模型 问题就是对之前的任务进行遗忘&#xff0c;在之前的任务上表现不好…

【机器学习】无监督学习算法之:K均值聚类

K均值聚类 1、引言2、K均值聚类2.1 定义2.2 原理2.3 实现方式2.4 算法公式2.4.1 距离计算公式2.4.1 中心点计算公式 2.5 代码示例 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c; K均值聚类 我不懂&#xff0c;能不能给我讲一讲&#xff1f; 小鱼&#xff1a;行&#xf…

AI助手 - Fitten Code

前言 上一篇介绍了商汤AI编程小助手小浣熊 Raccoon&#xff0c;过程中又发现了另外一款国产AI编程助手&#xff0c;那就是本篇要介绍的非十科技出品的Fitten Code。 ​ Fitten Code 主打一个快&#xff1a;超高准确率、超快的响应速度。号称代码生成比GitHub Copilot 快两倍&am…

蓝桥杯模块综合——高质量讲解AT24C02,BS18B20,BS1302,AD/DA(PCF8591),超声波模块

AT24C02——就是一个存储的东西&#xff0c;可以给他写东西&#xff0c;掉电不丢失。 void EEPROM_Write(unsigned char * EEPROM_String,unsigned char addr , unsigned char num) {IIC_Start();IIC_SendByte(0xA0);IIC_WaitAck();IIC_SendByte(addr);IIC_WaitAck();while(nu…

奶牛均分

解法&#xff1a; 假设编号从左到右递增&#xff0c;奶牛每次只能去往左边的牛圈。因此等分最大奶牛数小于等于最右边牛圈奶牛数&#xff0c;不妨设数为k&#xff0c;那么a[i]>k&#xff0c;a[i-1]>2k。。。 做后缀和二分答案就可找到k #include<iostream> #inc…

字符串筛选排序 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 输入一个由n个大小写字母组成的字符串&#xff0c; 按照 ASCII 码值从小到大的排序规则&#xff0c;查找字符串中第 k 个最小ASCII 码值的字母(k>1) , 输出该…

CSS学习(3)-浮动和定位

一、浮动 1. 元素浮动后的特点 脱离文档流。不管浮动前是什么元素&#xff0c;浮动后&#xff1a;默认宽与高都是被内容撑开&#xff08;尽可能小&#xff09;&#xff0c;而且可以设置宽 高。不会独占一行&#xff0c;可以与其他元素共用一行。不会 margin 合并&#xff0c;…

C语言易错知识点

1、数组长度及所占字节数 char x[] {"Hello"},y[]{H,e,l,l,o}; x数组的长度为5&#xff0c;y的长度也是5 x、y数组所占字符串为6为 51(\0)6 strlen&#xff08;&#xff09;函数得到的是数组的长度 2、%%与%的优先级 #include<stdio.h> int main(){ int a…

HarmonyOS4.0—自定义渐变导航栏开发教程

前言 今天要分享的是一个自定义渐变导航栏&#xff0c;本项目基于鸿蒙4.0。 先看效果&#xff1a; 这种导航栏在开发中也比较常见&#xff0c;特点是导航栏背景色从透明到不透明的渐变&#xff0c;以及导航栏标题和按钮颜色的变化。 系统的导航栏无法满足要求&#xff0c;我们…

Visual Studio 2013 - 高亮设置括号匹配 (方括号)

Visual Studio 2013 - 高亮设置括号匹配 [方括号] 1. 高亮设置 括号匹配 (方括号)References 1. 高亮设置 括号匹配 (方括号) 工具 -> 选项… -> 环境 -> 字体和颜色 References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

基于信号分解的几种一维时间序列降噪方法(MATLAB R2021B)

自适应信号分解算法是一种适合对非平稳信号分析的方法&#xff0c;它将一个信号分解为多个模态叠加的形式&#xff0c;进而可以准确反应信号中所包含的频率分量以及瞬时频率随时间变化的规律。自适应信号分解算法与众多“刚性”方法(如傅里叶变换&#xff0c;小波变换)不同&…

R语言实现多要素偏相关分析

偏相关分析是指当两个变量同时与第三个变量相关时&#xff0c;将第三个变量的影响剔除&#xff0c;只分析另外两个变量之间相关程度的过程&#xff0c;判定指标是相关系数的R值。 在GIS中&#xff0c;偏相关分析也十分常见&#xff0c;我们经常需要分析某一个指数与相关环境参…

浅谈一下对于DDD模式的理解2

浅谈一下对于DDD模式的理解&#xff0c;相互学习交流&#xff0c;不对之处欢迎大家指正。 在说到DDD(Domain-Driven Design)设计模式之前&#xff0c;先要说下我们在对系统进行架构设时需要遵循的几个原则&#xff1a; 单一职责&#xff08;SRP&#xff09; "单一职责原则…

原来这才是帕金森症状得到缓解的秘诀!

帕金森是一种影响神经系统的慢性疾病&#xff0c;主要症状包括震颤、肌肉僵硬和运动缓慢。如不及时治疗控制&#xff0c;症状可能会逐渐加重&#xff0c;严重影响生活质量。患者可能丧失自理能力&#xff0c;出现跌倒、骨折等并发症&#xff0c;还可能伴随认知障碍和情绪问题。…

考研数学|汤家凤《1800题》什么阶段做?值不值得做?

1800总的来说还是一本对基础不太好的同学一本不错的习题册&#xff0c;当然他可能对基础较好的同学来说题目量过大 考研数学备考&#xff0c;刷1800题是否必要&#xff1f;从我的经验来看&#xff0c;刷1800题并不是绝对必要的&#xff0c;而且传统习题册存在一些问题&#xf…

计算机组成原理 — 计算机的运算方法

计算机的运算方法 计算机的运算方法无符号数和有符号数概念有符号数有符号数又分真值和机器数原码表示法补码表示法反码表示法三种机器数的特点移码表示法 数的定点表示和浮点表示定点表示浮点表示 定点运算移位运算算数移位规则加法与减法运算乘法运算除法运算概述恢复余数法加…