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.Engineengine := gin.Default()// 注册中间件engine.Use(testMiddle)// 注册方法engine.Handle("GET", "/test", func(c *gin.Context) {fmt.Println("route test")})// 启动 http serverif 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.Pooltrees            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 = engineengine.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 HandlersChainbasePath stringengine   *Engineroot     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 {// 返回要注册的路由的绝对路径 absolutePathabsolutePath := group.calculateAbsolutePath(relativePath)// 返回要注册的路由的所有 handlershandlers = 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 {IRoutesGroup(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) IRoutesHandle(string, string, ...HandlerFunc) IRoutesAny(string, ...HandlerFunc) IRoutesGET(string, ...HandlerFunc) IRoutesPOST(string, ...HandlerFunc) IRoutesDELETE(string, ...HandlerFunc) IRoutesPATCH(string, ...HandlerFunc) IRoutesPUT(string, ...HandlerFunc) IRoutesOPTIONS(string, ...HandlerFunc) IRoutesHEAD(string, ...HandlerFunc) IRoutesMatch([]string, string, ...HandlerFunc) IRoutesStaticFile(string, string) IRoutesStaticFileFS(string, string, http.FileSystem) IRoutesStatic(string, string) IRoutesStaticFS(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.childrencs[pos].priority++prio := cs[pos].priority// Adjust position (move to front)newPos := posfor ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {// Swap node positionscs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]}// Build new index char stringif newPos != pos {n.indices = n.indices[:newPos] + // Unchanged prefix, might be emptyn.indices[pos:pos+1] + // The index char we moven.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]*TrieisEnd    bool
}func Constructor() Trie {return Trie{}
}

插入

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

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

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

查找

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

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

func (t *Trie) Search(word string) bool {node := trunes := []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 := trunes := []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 stringroot   *node
}type methodTrees []methodTreefunc (trees methodTrees) get(method string) *node {for _, tree := range trees {if tree.method == method {return tree.root}}return nil
}type node struct {fullPath  stringindices   stringchilderen []*nodehandlers  HandlersChain
}

挂载路由

其实就是 insert 操作

func (n *node) addRoute(path string, handlers HandlersChain) {runes := []rune(path)cur := nfor i := 0; i < len(runes); i++ {found := falsefor j, r := range []rune(cur.indices) {if r == runes[i] {cur = cur.childeren[j]found = truebreak}}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 = pathcur.handlers = handlers}}
}

查找路由

其实就是 search 操作

func (n *node) getValue(path string) (value nodeValue) {targetNode := n.search(path)if targetNode != nil {value.handlers = targetNode.handlersvalue.fullPath = targetNode.fullPath}return
}func (n *node) search(path string) *node {runes := []rune(path)cur := nfor i := 0; i < len(runes); i++ {found := falsefor j, r := range []rune(cur.indices) {if r == runes[i] {cur = cur.childeren[j]found = truebreak}}if !found {return nil}}return cur
}type nodeValue struct {handlers HandlersChainfullPath string
}

MethodTree 改进为 Patricia Trie

func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := pathfor {// 查找当前路径与节点路径的最长公共前缀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 = handlersreturn}}
}
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,和upportpath      string// 节点是否是参数节点,比如上面的:postwildChild bool// 节点类型,包括static, root, param, catchAll// static: 静态节点,比如上面的s,earch等节点// root: 树的根节点// catchAll: 有*匹配的节点// param: 参数节点nType     nodeType// 路径上最大参数个数maxParams uint8// 和children字段对应, 保存的是分裂的分支的第一个字符// 例如search和support, 那么s节点的indices对应的"eu"// 代表有两个分支, 分支的首字母分别是e和uindices   string// 儿子节点children  []*node// 处理函数handlers  HandlersChain// 优先级,子节点注册的handler数量priority  uint32
}

注册 handler

func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := path// 每有一个新路由经过此节点,priority 都要加 1n.priority++// 加入当前节点为 root 且未注册过子节点,则直接插入由并返回if len(n.path) == 0 && len(n.children) == 0 {n.insertChild(path, fullPath, handlers)n.nType = rootreturn}// 外层 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 对应的参数都要托付给 archindices:   n.indices,children: n.children,              handlers:  n.handlers,// 新路由 see 进入时,先将 search 的 priority 加 1 了,此时需要扣除 1 并赋给 archpriority:  n.priority - 1,fullPath:  n.fullPath,}// 先建立 search -> arch 的数据结构,后续调整 search 为 sen.children = []*node{&child}// 设置 se 的 indice 首字母为 an.indices = bytesconv.BytesToString([]byte{n.path[i]})// 调整 search 为 sen.path = path[:i]// search 的 handlers 都托付给 arch 了,se 本身没有 handlersn.handlers = nil           // ...}// 最长公共前缀长度小于 path,正如 se 之于 seeif i < len(path) {// path see 扣除公共前缀 se,剩余 epath = 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 之于 searchif 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 = pathn.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.pathif len(path) > len(prefix) {// node.path 长度 < path,且前缀匹配上if path[:len(prefix)] == prefix {// path 取为后半部分path = path[len(prefix):]// 遍历当前 node.indices,找到可能和 path 后半部分可能匹配到的 child nodeidxc := path[0]for i, c := range []byte(n.indices) {// 找到了首字母匹配的 child nodeif 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.fullPathreturn}// ...           }// 倘若 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 responseWriterRequest   *http.RequestWriter    ResponseWriter// ...handlers HandlersChainindex    int8fullPath stringengine       *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 = reqc.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 = nilcp.Writer = &cp.writermemcp.index = abortIndexcp.handlers = nilcp.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 = paramCopyreturn &cp
}

用 Context 处理请求

  1. 寻找对应的方法的方法树
  2. 在该方法树上通过 request url 找到对应的 handlers
  3. 将 handlers 封装到 Context 中
  4. 利用 c.Next() 调用 handlers 链处理请求
func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.Request.URL.Path// ...// Find root of the tree for the given HTTP methodt := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.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.Methodpath := c.Request.URLstatusCode := c.StatusCodelog.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
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#进阶-IIS应用程序池崩溃的解决方案

IIS是微软开发的Web服务器软件&#xff0c;被广泛用于Windows平台上的网站托管。在使用IIS过程中&#xff0c;可能会遇到应用程序池崩溃的问题&#xff0c;原因可能有很多&#xff0c;包括代码错误、资源不足、进程冲突等。本文将为大家介绍IIS应用程序池崩溃的问题分析和解决方…

目标检测损失函数:IoU、GIoU、DIoU、CIoU、EIoU、alpha IoU、SIoU、WIoU原理及Pytorch实现

前言 损失函数是用来评价模型的预测值和真实值一致程度&#xff0c;损失函数越小&#xff0c;通常模型的性能越好。不同的模型用的损失函数一般也不一样。损失函数主要是用在模型的训练阶段&#xff0c;如果我们想让预测值无限接近于真实值&#xff0c;就需要将损失值降到最低…

机器学习系列--R语言随机森林进行生存分析(1)

随机森林&#xff08;Breiman 2001a&#xff09;&#xff08;RF&#xff09;是一种非参数统计方法&#xff0c;需要没有关于响应的协变关系的分布假设。RF是一种强大的、非线性的技术&#xff0c;通过拟合一组树来稳定预测精度模型估计。随机生存森林&#xff08;RSF&#xff0…

HTML与CSS

目录 1、HTML简介 2、CSS简介 2.1选择器 2.1.1标签选择器 2.1.2类选择器 2.1.3层级选择器(后代选择器) 2.1.4id选择器 2.1.5组选择器 2.1.6伪类选择器 2.2样式属性 2.2.1布局常用样式属性 2.2.2文本常用样式属性 1、HTML简介 超文本标记语言HTML是一种标记语言&…

Python 从入门到精通之通俗易懂学闭包

系列 Python从入门到精通之安装与快速入门-CSDN博客 Python从入门到精通之基本数据类型和变量-CSDN博客 Python从入门到精通之集合&#xff08;List列表、Tuple元组、Dict字典、Set&#xff09;-CSDN博客 Python从入门到精通之条件语句、循环语句和函数-CSDN博客 Python从…

中央集成式架构量产时代,openVOC方案将引发软件开发模式变革

2024年&#xff0c;中央计算区域控制架构正式进入规模化量产周期&#xff0c;汽车智能化正式迈入2.0时代&#xff0c;产业生态、应用创新、开发模式都将迎来巨大变革。 同时&#xff0c;随着ChatGPT引发的AIGC领域的爆发式增长&#xff0c;人工智能技术掀起全球万亿级信息化应…

Cookie的详解使用(创建,获取,销毁)

文章目录 Cookie的详解使用&#xff08;创建&#xff0c;获取&#xff0c;销毁&#xff09;1、Cookie是什么2、cookie的常用方法3、cookie的构造和获取代码演示SetCookieServlet.javaGetCookieServlet.javaweb.xml运行结果如下 4、Cookie的销毁DestoryCookieServletweb.xml运行…

[OCR]Python 3 下的文字识别CnOCR

目录 1 CnOCR 2 安装 3 实践 1 CnOCR CnOCR 是 Python 3 下的文字识别&#xff08;Optical Character Recognition&#xff0c;简称OCR&#xff09;工具包。 工具包支持简体中文、繁体中文&#xff08;部分模型&#xff09;、英文和数字的常见字符识别&#xff0c;支持竖…

记一次接口交互is开头的属性序列化后“is”丢失问题

问题背景&#xff1a; 今天在做项目联调时调用别人的第三方接口时&#xff0c;发现字段传递不对导致参数传递异常的问题&#xff0c;当时还很奇怪&#xff0c;明白传好着呢&#xff0c;怎么就好端端的出现字段不对的情况呢&#xff1f; 查看发现该字段为boolean类型的isIsRef…

DsPdf:GcPdf 7.0 for NET Crack

DsPdf:GcPdf 7.0 用于全面文档控制的功能丰富的 C# .NET PDF API 库 PDF 文档解决方案&#xff08;DsPdf&#xff0c;以前称为 GcPdf&#xff09;可让您快速、高效地生成文档&#xff0c;且无需依赖任何内存。 在 C# .NET 中生成、加载、编辑和保存 PDF 文档 支持多种语言的全…

爬虫详细教程第1天

爬虫详细教程第一天 1.爬虫概述1.1什么是爬虫&#xff1f;1.2爬虫工具——Python1.3爬虫合法吗&#xff1f;1.4爬虫的矛与盾1.4.1反爬机制1.4.2反爬策略1.4.3robots.txt协议 2.爬虫使用的软件2.1使用的开发工具: 3.第一个爬虫4.web请求4.1讲解一下web请求的全部过程4.2页面渲染…

test mock-03-wiremock 模拟 HTTP 服务的开源工具 flexible and open source API mocking

拓展阅读 test 之 jmockit-01-overview jmockit-01-test 之 jmockit 入门使用案例 mockito-01-overview mockito 简介及入门使用 PowerMock Mock Server ChaosBlade-01-测试混沌工程平台整体介绍 jvm-sandbox 入门简介 wiremock WireMock是一个流行的开源工具&#xf…

git(安装,常用命令,分支操作,gitee,IDEA集成git,IDEA集成gitee,IDEA集成github,远程仓库操作)

文章目录 1. Git概述1.1 何为版本控制1.2 为什么需要版本控制1.3 版本控制工具1.4 Git简史1.5 Git工作机制1.6 Git和代码托管中心 2. Git安装3. Git常用命令3.1 设置用户签名3.1.1 说明3.1.2 语法3.1.3 案例实操 3.2 初始化本地库3.2.1 基本语法3.2.2 案例实操3.2.3 结果查看 3…

【瞎折腾/3D】无父物体下物体的旋转与移动

目录 说在前面移动World SpaceLocal Space 旋转World SpaceLocal Space 代码 说在前面 测试环境&#xff1a;Microsoft Edge 120.0.2210.91three.js版本&#xff1a;0.160.0其他&#xff1a;本篇文章中只探讨了无父对象下的移动与旋转&#xff0c;有父对象的情况将在下篇文章中…

Python中的用户交互函数详解,提升用户体验!

更多Python学习内容&#xff1a;ipengtao.com 用户进行交互的Python应用程序&#xff0c;有许多常用的用户交互函数可以帮助创建更具吸引力和友好的用户界面。本文将介绍一些常用的Python用户交互函数&#xff0c;并提供详细的示例代码&#xff0c;以帮助大家更好地理解它们的用…

kubeadm来搭建k8s集群。

我们采用了二进制包搭建出的k8s集群&#xff0c;本次我们采用更为简单的kubeadm的方式来搭建k8s集群。 二进制的搭建更适合50台主机以上的大集群&#xff0c;kubeadm更适合中小型企业的集群搭建 主机配置建议&#xff1a;2c 4G 主机节点 IP …

学习动态规划解决不同路径、最小路径和、打家劫舍、打家劫舍iii

学习动态规划|不同路径、最小路径和、打家劫舍、打家劫舍iii 62 不同路径 动态规划&#xff0c;dp[i][j]表示从左上角到(i,j)的路径数量dp[i][j] dp[i-1][j] dp[i][j-1] import java.util.Arrays;/*** 路径数量* 动态规划&#xff0c;dp[i][j]表示从左上角到(i,j)的路径数量…

【JavaScript】垃圾回收与内存泄漏

✨ 专栏介绍 在现代Web开发中&#xff0c;JavaScript已经成为了不可或缺的一部分。它不仅可以为网页增加交互性和动态性&#xff0c;还可以在后端开发中使用Node.js构建高效的服务器端应用程序。作为一种灵活且易学的脚本语言&#xff0c;JavaScript具有广泛的应用场景&#x…

【ArcGIS微课1000例】0082:地震灾害图件制作之DEM晕渲图(山体阴影效果)

以甘肃积石山县6.2级地震为例,基于震中100km范围内的DEM数据,制作数字高程模型山体阴影晕渲图。 文章目录 一、效果展示二、实验数据三、晕渲图制作一、效果展示 基于数字高程模型制作的山体阴影晕渲图如下所示: 二、实验数据 本试验所需要的数据包括: 1. 震中位置矢量数…

【JavaFX】JDK11 基于Gson、hutool、Jackson持久化存储实体类数据的解决方案 (读取、追加、去重json对象)

文章目录 开发环境效果前言一、Gson是什么?二、使用步骤1.引入依赖2.创建实体类创建 JsonFileService类创建JsonFileService的实现类 JsonFileServiceImpl三、实现效果开发环境 JDK11IDEA 2023.3Gson、hutool、JacksonJavaFX 11效果 前言 使用JDK1