Gin 源码深度解析及实现

在这里插入图片描述

介绍

什么是 gin ?

一个轻量级高性能 HTTP Web 框架。

Introduction | Gin Web Framework (gin-gonic.com)
Gin 是一个用 Go (Golang) 编写的 HTTP Web 框架。 它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。

为什么使用 gin ?

Introduction | Gin Web Framework (gin-gonic.com)

  • Zero allocation router
  • Fast
  • Middleware support
  • Crash-free
  • JSON validation
  • Routes grouping
  • Error management
  • Rendering built-in
  • Extendable

Quickstart

  1. 构造默认配置的 gin.Engine
  2. 注册中间件
  3. 注册方法
  4. 启动 http server
func testMiddle(c *gin.Context) {
	fmt.Println("middle test")
}

func main() {
	// 构造默认配置的 gin.Engine
	engine := gin.Default()
	// 注册中间件
	engine.Use(testMiddle)
	// 注册方法
	engine.Handle("GET", "/test", func(c *gin.Context) {
		fmt.Println("route test")
	})
	// 启动 http server
	if err := engine.Run(); err != nil {
		fmt.Println(err)
	}
}

gin 源码模块

位置功能
gin.go核心结构体 Engine,整个服务的入口
context.go请求上下文
tree.go方法树,底层是压缩前缀树
routergroup.go路由组

为了方便大家阅读,实现了一个非常简易版的 gin
https://github.com/ddl-killer/gin-read/tree/main/simplified_gin/lorgin

gin 与 net/http

gin 是对 net/http 做了一层包装,使其更加简单易用

为什么不用 net/http?

在有 net/http 的情况下,是否有任何真正令人信服的理由来引入像 gin 这样的外部依赖

在 go 版本更新 1.22 之前,可能可以有如下理由:

  • 需要支持基于方法的路由
  • 需要支持 url 路径中的变量
  • 需要支持基于主机的路由选择
  • 需要支持 regexp 路由模式
  • 需要构建一个大量使用中间件的 web 程序

之前由于 net/http 包的路由注册能力较为原生,以致于诞生了不少第三方库以增强路由注册能力,但在这个月底刚更新的 go1.22 里增强了路由注册能力,支持了可选方法,设置路径变量和基于主机的路由注册。

"我应该使用哪个路由器软件包?"是 Go 程序初学者的常见问题。在 Go 1.22 发布后,这个问题的常见答案会有所改变,因为许多人会发现标准库足以满足他们的需求,而无需求助于第三方软件包。

但是 gin 这样的轻量级框架,它不仅提供了路由器,还提供了用于构建 Web 后端的额外工具, 比如更便捷的中间件注册,错误处理和模板渲染。

由 log/slog 的作者 Jonathan Amsterdam 创建的提案
https://github.com/golang/go/issues/61410

https://tip.golang.org/doc/go1.22#enhanced_routing_patterns

Enhanced routing patterns

HTTP routing in the standard library is now more expressive. The patterns used by net/http.ServeMux have been enhanced to accept methods and wildcards.

Registering a handler with a method, like "POST /items/create", restricts invocations of the handler to requests with the given method. A pattern with a method takes precedence over a matching pattern without one. As a special case, registering a handler with "GET" also registers it with "HEAD".

Wildcards in patterns, like /items/{id}, match segments of the URL path. The actual segment value may be accessed by calling the Request.PathValue method. A wildcard ending in “…”, like /files/{path...}, must occur at the end of a pattern and matches all the remaining segments.

A pattern that ends in “/” matches all paths that have it as a prefix, as always. To match the exact pattern including the trailing slash, end it with {$}, as in /exact/match/{$}.

If two patterns overlap in the requests that they match, then the more specific pattern takes precedence. If neither is more specific, the patterns conflict. This rule generalizes the original precedence rules and maintains the property that the order in which patterns are registered does not matter.

This change breaks backwards compatibility in small ways, some obvious—patterns with “{” and “}” behave differently— and some less so—treatment of escaped paths has been improved. The change is controlled by a GODEBUG field named httpmuxgo121. Set httpmuxgo121=1 to restore the old behavior.

https://pkg.go.dev/net/http@go1.22rc1#ServeMux

Patterns

Patterns can match the method, host and path of a request. Some examples:

  • “/index.html” matches the path “/index.html” for any host and method.
  • “GET /static/” matches a GET request whose path begins with “/static/”.
  • “example.com/” matches any request to the host “example.com”.
  • “example.com/{$}” matches requests with host “example.com” and path “/”.
  • “/b/{bucket}/o/{objectname…}” matches paths whose first segment is “b” and whose third segment is “o”. The name “bucket” denotes the second segment and “objectname” denotes the remainder of the path.

gin 是如何与 net/http 链接起来的呢?

处理请求

net/http 中最终会通过调用 ServeHTTP 方法处理请求,所以实现该 Handler 接口即可

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
func (c *conn) serve(ctx context.Context) {
  for {
    w, err := c.readRequest(ctx)
    serverHandler{c.server}.ServeHTTP(w, w.req)
    w.finishRequest()
  }
}

gin 的核心结构体 Engine 即实现了该接口

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	engine.handleHTTPRequest(c)
}

启动服务

通过 Engine.Run() 启动 http server 的背后其实是通过 http.ListenAndServe() 启动

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

Overview

在这里插入图片描述

主体流程

  1. 初始化 Engine 实例
  2. 注册 middleware
  3. 注册 handler
  4. 启动服务

在这里插入图片描述

初始化 Engine 实例

Default() 除了一些默认的配置之外,其实就是调用 New() 创建 Engine 实例

在 New() 中,将 RouterGroup,trees,和 pool 初始化好

  1. RouterGroup 是路由组,负责在服务启动前的辅助注册路由
  2. trees 是方法树,底层是一个压缩前缀树 radix,上面的叶子节点挂载的就是 url 及其对应 url 的请求处理函数,处理请求的时候就在方法树上通过请求的 url 查找对应 url 的请求处理函数
  3. pool 是 gin.Context 对象池,gin.Context 也是使用 gin 框架中经常会使用到的结构体,会通过 ServeHTTP 函数的参数和注册的路由和中间件构建成请求上下文 gin.Context
type Engine struct {
	RouterGroup
	// ...
	pool             sync.Pool
	trees            methodTrees
	// ...
}
func Default() *Engine {
	// ...
	engine := New()
	// 一些默认配置
	return engine
}
func New() *Engine {
	// ...
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
        // ...
		trees:                  make(methodTrees, 0, 9),
		// ...
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() any {
		return engine.allocateContext(engine.maxParams)
	}
	return engine
}

注册 middleware

Engine.Use -》RouterGroup.Use 然后将新注册的 middleware 拼接到 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()
}
type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}
type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc

注册 handler

  1. group.calculateAbsolutePath(relativePath) 返回要注册的路由的绝对路径 absolutePath
  2. group.combineHandlers(handlers) 返回要注册的路由的所有 handlers
  3. group.engine.addRoute(httpMethod, absolutePath, handlers) 调用 Engine.addRoute() 将路由挂载到方法树上
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	// ...
	return group.handle(httpMethod, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// 返回要注册的路由的绝对路径 absolutePath
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 返回要注册的路由的所有 handlers
	handlers = group.combineHandlers(handlers)
    // 调用 Engine.addRoute() 将路由挂载到方法树上
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

计算要注册的路由的绝对路径

拼接 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
}

计算要注册的路由的所有 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
}

挂载路由到方法树上

  1. 获取对应 method 的方法树
  2. 将 path 和对应的 handlers 挂载到该方法树上
  3. root.addRoute(path, handlers) 这步就是压缩前缀树添加节点的算法,后面再展开
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	// ...
    // 获取对应 method 的方法树
	root := engine.trees.get(method)
	if root == nil {
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
    // 将 path 和对应的 handlers 挂载到该方法树上
	root.addRoute(path, handlers)
	// ...
}

启动 http server

通过 Engine.Run() 启动 http server 的背后其实是通过 http.ListenAndServe() 启动

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

在这里插入图片描述

RouterGroup

路由分组

通过 IRouter 接口的 Group(string, …HandlerFunc) *RouterGroup 方法分出路由组

和注册 handler 的过程异曲同工

  1. 获取新路由组的完整路径
  2. 获取新路由的所有方法
type IRouter interface {
	IRoutes
	Group(string, ...HandlerFunc) *RouterGroup
}
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

注册 handler

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
	Match([]string, string, ...HandlerFunc) IRoutes

	StaticFile(string, string) IRoutes
	StaticFileFS(string, string, http.FileSystem) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

MethodTree

方法树的底层是具有优先级的压缩前缀树

Trie 前缀树

1960 年,爱德华-弗雷德金(Edward Fredkin)创造了 trie 一词,并在检索的中间音节后发音为 /ˈtriː/ (如 “tree”)。 不过,其他作者将其发音为 /ˈtraɪ/ (如 “try”),试图在口头上将其与 "tree "区分开来。Tries 是字符串索引查找数据结构的一种形式,用于存储单词的字典列表,可以高效地生成补全列表。所以也被称为字典树,但我不喜欢字典树这个发音,因为我对字典的印象是一本扁的书,和树这种立体结构不搭边。

通过下图可以看出,前缀树的关键点就是每一个节点的所有的子节点都拥有相同的前缀。

对于 trie 的应用场景主要就是一次建树,多次查找

插入时间复杂度:O(n)

查找时间复杂度:O(n)

路由注册也可以使用 hashmap,为什么用前缀树?

  1. 动态路由:网络框架通常需要支持动态路由,在这种情况下,路径可以有不同的段。例如, `/users/:userid/order 。hashmap 对于静态路由很有效,但并不支持这种模式匹配。

Patricia Trie / Radix 压缩前缀树

压缩前缀树是一种优化过的树结构,用于高效地存储和检索具有共享前缀的字符串。在这种树中,每个节点可以包含多个字符,从而减少树的深度并提高效率。

为什么用压缩前缀树?

  1. 减少树的深度并提高查找效率
  2. 因为减少树的深度同时也能减少内存占用

在这里插入图片描述

PATRICIA–检索字母数字编码信息的实用算法
https://dl.acm.org/doi/pdf/10.1145/321479.321481

出版日期:1968 年 10 月 01 日

具有优先级的 Patricia Trie

因为前缀树的查找是一个从左到右深度遍历树的过程,所以如果一个被查找概率大的节点但排在最右边,就会增加查找的耗时。

因此会增加特定子节点的优先级,并在必要时重新排序。这对于处理具有相同前缀但不同优先级的路由至关重要。

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
}

Merkle Patricia Trie 默克尔压缩前缀树

虽然都是树形数据结构,但它们的设计目的和特性有显著的不同

Gin 框架中的 radix 主要用于高效的路由匹配。它通过共享公共前缀来组织路由,优化了路径的搜索和匹配过程。

以太坊中的MPT主要用于确保数据的完整性和一致性。根节点成为整个数据结构的加密指纹。它是一种加密的数据结构,用于验证区块链中的数据没有被篡改。专注于确保区块链数据的安全和一致性。

实现 Trie

type Trie struct {
	Children [26]*Trie
	isEnd    bool
}

func Constructor() Trie {
	return Trie{}
}

插入

描述:向 Trie 中插入一个单词 word

实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

func (t *Trie) Insert(word string) {
	node := t
	runes := []rune(word)
	for i := 0; i < len(runes); i++ {
		idx := int(runes[i]) - 'a'
		if node.Children[idx] == nil {
			child := Constructor()
			node.Children[idx] = &child
			node = &child
		} else {
			node = node.Children[idx]
		}
	}
	node.isEnd = true
}

查找

描述:查找 Trie 中是否存在单词 word

实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回 false,如果匹配到了最后一个字符,那我们只需判断 node->isEnd即可。

func (t *Trie) Search(word string) bool {
	node := t
	runes := []rune(word)
	for i := 0; i < len(runes); i++ {
		idx := int(runes[i]) - 'a'
		if node.Children[idx] == nil {
			return false
		} else {
			node = node.Children[idx]
		}
	}
	return node.isEnd
}

实现查找的过程中,我踩了一个坑

在这里插入图片描述

左图的情况下,比如 insert abcd,search abcda,也会返回 true,这是不预期的

前缀匹配

描述:判断 Trie 中是或有以 prefix 为前缀的单词

实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。

func (t *Trie) StartsWith(prefix string) bool {
	node := t
	runes := []rune(prefix)
	for i := 0; i < len(runes); i++ {
		idx := int(runes[i]) - 'a'
		if node.Children[idx] == nil {
			return false
		} else {
			node = node.Children[idx]
		}
	}
	return true
}

基于 Trie 实现 MethodTree

定义结构体如下

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree

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

type node struct {
	fullPath  string
	indices   string
	childeren []*node
	handlers  HandlersChain
}

挂载路由

其实就是 insert 操作

func (n *node) addRoute(path string, handlers HandlersChain) {
	runes := []rune(path)
	cur := n
	for i := 0; i < len(runes); i++ {
		found := false
		for j, r := range []rune(cur.indices) {
			if r == runes[i] {
				cur = cur.childeren[j]
				found = true
				break
			}
		}
		if !found {
			cur.indices += string(runes[i])
			child := &node{
				fullPath: "",
				indices:  "",
				handlers: nil,
			}
			cur.childeren = append(cur.childeren, child)
			cur = child
		}
		if i == len(runes)-1 {
			cur.fullPath = path
			cur.handlers = handlers
		}
	}
}

查找路由

其实就是 search 操作

func (n *node) getValue(path string) (value nodeValue) {
	targetNode := n.search(path)
	if targetNode != nil {
		value.handlers = targetNode.handlers
		value.fullPath = targetNode.fullPath
	}
	return
}

func (n *node) search(path string) *node {
	runes := []rune(path)
	cur := n
	for i := 0; i < len(runes); i++ {
		found := false
		for j, r := range []rune(cur.indices) {
			if r == runes[i] {
				cur = cur.childeren[j]
				found = true
				break
			}
		}
		if !found {
			return nil
		}
	}
	return cur
}

type nodeValue struct {
	handlers HandlersChain
	fullPath string
}

MethodTree 改进为 Patricia Trie

func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path

    for {
        // 查找当前路径与节点路径的最长公共前缀
        i := longestCommonPrefix(path, n.path)

        // 如果当前节点的路径不是最长公共前缀的一部分,则需要分割当前节点
        if i < len(n.path) {
            // 创建一个新的子节点来保存当前节点的剩余部分
            child := node{
                path:     n.path[i:], // 分割后的剩余路径
                indices:  n.indices,
                children: n.children,
                handlers: n.handlers,
            }

            // 更新当前节点的路径为最长公共前缀,并重置其子节点和处理程序
            n.children = []*node{&child}
            n.indices = string(n.path[i])
            n.path = n.path[:i]
            n.handlers = nil
        }

        // 如果有剩余路径需要处理
        if i < len(path) {
            path = path[i:]

            // 寻找下一个匹配的子节点
            idx := strings.IndexByte(n.indices, path[0])
            if idx < 0 {
                // 如果没有匹配的子节点,创建一个新的子节点
                child := &node{path: path, handlers: handlers}
                n.indices += string(path[0]) // 更新索引字符
                n.children = append(n.children, child) // 将新子节点添加到子节点列表
                return
            }

            // 如果找到匹配的子节点,继续在该子节点上处理剩余路径
            n = n.children[idx]
        } else {
            // 如果路径已经完全匹配,设置当前节点的处理程序
            n.handlers = handlers
            return
        }
    }
}

func longestCommonPrefix(a, b string) int {
	max := len(a)
	if len(b) < max {
		max = len(b)
	}
	for i := 0; i < max; i++ {
		if a[i] != b[i] {
			return i
		}
	}
	return max
}

gin’s MethodTree

Struct

type node struct {
    // 节点路径,比如上面的s,earch,和upport
    path      string
    // 节点是否是参数节点,比如上面的:post
    wildChild bool
    // 节点类型,包括static, root, param, catchAll
    // static: 静态节点,比如上面的s,earch等节点
    // root: 树的根节点
    // catchAll: 有*匹配的节点
    // param: 参数节点
    nType     nodeType
    // 路径上最大参数个数
    maxParams uint8
    // 和children字段对应, 保存的是分裂的分支的第一个字符
    // 例如search和support, 那么s节点的indices对应的"eu"
    // 代表有两个分支, 分支的首字母分别是e和u
    indices   string
    // 儿子节点
    children  []*node
    // 处理函数
    handlers  HandlersChain
    // 优先级,子节点注册的handler数量
    priority  uint32
}

注册 handler

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
    // ...
}

查找 handler

// 从路由树中获取 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 并贯穿整个请求到响应的过程

  1. Request :请求
  2. Writer :响应的 writer
  3. handlers: 这次请求的所有 handlers,是通过 request 的 url 从方法树中获取的
  4. index :handlers 执行的游标
  5. fullPath:完整 url
  6. mu 和 Keys:存储 map 和对应的读写锁
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter
    
    // ...

	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine

	// This mutex protects Keys map.
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]any
    
    // ...
}

复用 Context

Context.pool 是 sync.Pool 对象, sync.Pool 的应用场景是保存和复用临时对象,减少内存分配,降低 GC 压力。

在网络通信过程中,当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。这样就可以不用再次经过内存分配,可直接复用已有对象,减轻 GC 的压力,从而提升系统的性能。

前段时间刚接触 chromedp, 当时需要加快通过 chromedp 访问网页的效率,所以这里想通过浏览器访问网页来举例 Context 的复用。浏览器每次访问一个新的网页的过程,都需要新开一个标签页和关闭该标签页。想要增加 chromedp 访问网页的效率可以通过将复用访问完的标签页,而不需要重复的打开和关闭标签页。这里的 Context 就类似标签页,需要被处理的请求就是一个个需要被访问的网页,sync.Pool 就是一个被复用的标签页组。

  1. engine.pool.Get().(*Context) :用于从 gin.Context 对象池中获取对象

  2. c.writermem.reset(w)
    c.Request = req
    c.reset()

重新封装 gin.Context

  1. engine.handleHTTPRequest©:传入 gin.Context 并处理请求

  2. engine.pool.Put©:将 gin.Context 放回对象池

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

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}
type Context struct {
    // ....
    pool sync.Pool
    // ...
}

因为 Context 会被复用,所以 Context 的复制必须是深复制,源代码的注释也强调了

Copy returns a copy of the current context that can be safely used outside the request’s scope.

// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
	cp := Context{
		writermem: c.writermem,
		Request:   c.Request,
		Params:    c.Params,
		engine:    c.engine,
	}
	cp.writermem.ResponseWriter = nil
	cp.Writer = &cp.writermem
	cp.index = abortIndex
	cp.handlers = nil
	cp.Keys = map[string]any{}
	for k, v := range c.Keys {
		cp.Keys[k] = v
	}
	paramCopy := make([]Param, len(cp.Params))
	copy(paramCopy, cp.Params)
	cp.Params = paramCopy
	return &cp
}

用 Context 处理请求

  1. 寻找对应的方法的方法树
  2. 在该方法树上通过 request url 找到对应的 handlers
  3. 将 handlers 封装到 Context 中
  4. 利用 c.Next() 调用 handlers 链处理请求
func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
	
    // ...

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
		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
	}

	// ...
	serveError(c, http.StatusNotFound, default404Body)
}
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

利用 c.Next()

利用 Next() 可以实现 handler 方法的压栈,以实现对一个请求的前置处理和后置处理

用 Logger() 中间件举例,请求处理前记录开始时间,请求处理后记录耗时

在这里插入图片描述

func middleware HandlerFunc {
	return func(c *Context) {
        // 前处理
        preHandle()  
        c.Next()
        // 后处理
        postHandle()
	}
}
func Logger() HandlerFunc {
	return func(c *Context) {
		start := time.Now()
		c.Next()

		method := c.Request.Method
		path := c.Request.URL
		statusCode := c.StatusCode
		log.Println("method:", method, "path:", path, "statusCode:", statusCode, "cost:", time.Since(start))
	}
}

利用 Next() 可以实现提前熔断,取消执行后续的 handler

handler 的长度被设置为 63,也就是说 handler 调用链的长度一定不会大于 63,当数字比这个大的时候就可以提前退出 for 循环

func (c *Context) Abort() {
    c.index = abortIndex
}
const abortIndex int8 = math.MaxInt8 >> 1
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
}

数据存储


// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes  c.Keys if it was not used previously.
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
}

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	value, exists = c.Keys[key]
	return
}

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

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

相关文章

基于CNN和双向gru的心跳分类系统

CNN and Bidirectional GRU-Based Heartbeat Sound Classification Architecture for Elderly People是发布在2023 MDPI Mathematics上的论文&#xff0c;提出了基于卷积神经网络和双向门控循环单元(CNN BiGRU)注意力的心跳声分类&#xff0c;论文不仅显示了模型还构建了完整的…

车联网的安全风险与应对措施

安全风险 1、恶意软件 攻击者可以通过入侵厂商或供应商网络&#xff0c;用恶意软件&#xff08;如病毒、木马、勒索软件等&#xff09;感染车联网系统组件&#xff0c;从而获得对车辆的控制权或窃取敏感信息。例如&#xff0c;一名安全研究人员成功入侵了特斯拉&#xff08;T…

Vue使用Element table表格格式化GMT时间为Shanghai时间

Vue使用Element表格格式化GMT时间为Shanghai时间 说明 阿里巴巴java开发规范规定&#xff0c;数据库必备gmt_create、gmt_modified字段&#xff0c;使用的是GMT时间&#xff0c;在中国使用必然要转换我中国时间。 在阿里巴巴的Java开发规范中&#xff0c;要求每个表都必备三…

【C语言深度剖析——第一节(关键字1)】《C语言深度解剖》+蛋哥分析+个人理解

你未曾见过火光&#xff0c;难怪甘愿漂泊寒夜 本文由睡觉待开机原创&#xff0c;未经允许不得转载。 本内容在csdn网站首发 欢迎各位点赞—评论—收藏 如果存在不足之处请评论留言&#xff0c;共同进步&#xff01; 首先简单介绍一下《C语言深度解剖》&#xff1a; 全书特点&am…

一起玩儿物联网人工智能小车(ESP32)——19. MicroPython集成开发环境Thonny的安装

摘要&#xff1a;本文主要介绍MicroPython集成开发环境Thonny的安装方法和基本的使用 Thonny是一个开源的、轻量级的MicroPython集成开发环境&#xff0c;它非常的小巧和精简。对于我们日常ESP32的开发来说&#xff0c;已经完全满足要求了。这个开发软件支持windows、Linux和Ma…

聚焦企业未来新发展,同花顺对话麒麟信安杨涛董事长

打好经济增长主动仗 聚焦企业未来新发展 同花顺“对话湖南上市公司高管系列活动” 本期走进麒麟信安 对话麒麟信安杨涛董事长 畅谈国产操作系统领军企业的创新发展之路 并就麒麟信安产品应用布局及 未来发展规划等方面进行深入交流 麒麟信安-成片1改20231218

统一入口,全面提升工作效率,WorkPlus超级APP引领数字化办公新潮流

在数字化办公的时代&#xff0c;企业往往面临着多个应用管理的挑战。员工需要在众多应用之间切换&#xff0c;记忆不同的登录凭证&#xff0c;这不仅分散了注意力&#xff0c;还降低了工作效率。为了应对这一问题&#xff0c;企业需要一站式的解决方案&#xff0c;通过整合繁杂…

盘点2023 | 校企合作结硕果,产教融合谱新篇

回首2023&#xff0c;电巢科技与众多高校建立了紧密的合作关系&#xff0c;以实习就业为导向&#xff0c;帮助学生打开技术和产业视野&#xff0c;提前做好职业发展规划&#xff0c;按照电子行业的企业用人标准&#xff0c;帮助高校进行“人才前置化”培养&#xff0c;并且持续…

4.Python数据序列

Python数据序列 一、作业回顾 1、面试题 有一物,不知其数,三三数之余二,五五数之余三,七七数之余二,问物几何? 白话文:有一个数字,不知道具体是多少,用3去除剩2,用5去除剩3,用7去除剩2个,问这个数是多少?1 ~ 100以内的整数 while循环: # 初始化计数器 i = …

阿里云PolarDB数据库费用价格表

阿里云数据库PolarDB租用价格表&#xff0c;云数据库PolarDB MySQL版2核4GB&#xff08;通用&#xff09;、2个节点、60 GB存储空间55元5天&#xff0c;云数据库 PolarDB 分布式版标准版2核16G&#xff08;通用&#xff09;57.6元3天&#xff0c;阿里云百科aliyunbaike.com分享…

人工智能的基础-深度学习

什么是深度学习? 深度学习是机器学习领域中一个新的研究方向&#xff0c;它被引入机器学习使其更接近于人工智能。 深度学习是机器学习领域中一个新的研究方向&#xff0c;它被引入机器学习使其更接近于最初的目标——人工智能。 深度学习是学习样本数据的内在规律和表示层次&…

uni-app tabbar组件

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…

Android移动端超分辨率调研(未完成 目前自用)

作用 图片加载是目前几乎所有的APP都具备的基础能力&#xff0c;在节省服务商的传输带宽之外&#xff0c;也可以降低用户消费端流量的消耗&#xff0c;提升用户的加载速度。帮助每一个产品用更低的成本达到更好的图片加载效果。 效果 另一方面 用TensorFlow实现的图像极度压…

【Python】pip下载源修改

运行 pip install 命令 会从网站上下载指定的python包&#xff0c;默认是从 https://files.pythonhosted.org/ 网站上下载。 https://files.pythonhosted.org 这是个国外的网站&#xff0c;遇到网络情况不好的时候&#xff0c;可能会下载失败&#xff0c;我们可以通过命令&am…

分布式系统架构设计之分布式系统架构演进和版本管理

在分布式系统的生命周期中&#xff0c;架构演进和版本管理是很重要的两个环节。本部分会介绍分布式系统架构演进的原则、策略以及版本管理的最佳实践&#xff0c;以帮助研发团队更好地应对需求变化、技术发展和系统升级。 架构演进 演进原则 渐进式演进 采用渐进式演进的原…

vue本地缓存搜索记录(最多4条)

核心代码 //保存到搜索历史&#xff0c;最多存四个 item.name和item.code格式为&#xff1a;塞力斯000001var history uni.getStorageSync(history) || [];console.log("history", history)var index history.findIndex((items) > {return item.name items.nam…

Goby 漏洞发布| QNAP NAS authLogin.cgi 命令执行漏洞(CVE-2017-6361)

漏洞名称&#xff1a;QNAP NAS authLogin.cgi 命令执行漏洞&#xff08;CVE-2017-6361&#xff09; English Name&#xff1a;QNAP NAS authLogin.cgi command execution vulnerability (CVE-2017-6361) CVSS core: 9.8 影响资产数&#xff1a; 2637547 漏洞描述&#xff1…

分享72个Python爬虫源码总有一个是你想要的

分享72个Python爬虫源码总有一个是你想要的 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1v2P4l5R6KT2Ul-oe2SF8cw?pwd6666 提取码&#xff1a;6666 项目名称 10 photo websites…

如何在Docker环境下安装火狐浏览器并结合内网穿透工具实现公网访问

文章目录 1. 部署Firefox2. 本地访问Firefox3. Linux安装Cpolar4. 配置Firefox公网地址5. 远程访问Firefox6. 固定Firefox公网地址7. 固定地址访问Firefox Firefox是一款免费开源的网页浏览器&#xff0c;由Mozilla基金会开发和维护。它是第一个成功挑战微软Internet Explorer浏…

【C++入门(一)】:详解C++语言的发展及其重要性

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; C入门到进阶 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f324;️什么是C&#x1f324;️C的发展史&#x1f324;️C的重要性☁️语言的广泛度☁️C的领域⭐…