Gin 源码概览 - 路由

server/2025/1/21 18:16:15/

本文基于gin 1.1 源码解读
https://github.com/gin-gonic/gin/archive/refs/tags/v1.1.zip

1. 注册路由

我们先来看一段gin代码,来看看最终得到的一颗路由树长啥样

func TestGinDocExp(t *testing.T) {engine := gin.Default()engine.GET("/api/user", func(context *gin.Context) {fmt.Println("api user")})engine.GET("/api/user/info/a", func(context *gin.Context) {fmt.Println("api user info a")})engine.GET("/api/user/information", func(context *gin.Context) {fmt.Println("api user information")})engine.Run()
}

看起来像是一颗前缀树,我们后面再仔细深入源码

ginDefault_26">1.1 gin.Default

gin.Default() 返回了一个Engine 结构体指针,同时添加了2个函数,LoggerRecovery

func Default() *Engine {engine := New()engine.Use(Logger(), Recovery())return engine
}

New方法中初始化了 Engine,同时还初始化了一个RouterGroup结构体,并将Engine赋值给RouterGroup,相当于互相套用了

func New() *Engine {engine := &Engine{RouterGroup: RouterGroup{Handlers: nil,  // 业务handle,也可以是中间件basePath: "/",  // 根地址root:     true, // 根路由},RedirectTrailingSlash:  true,RedirectFixedPath:      false,HandleMethodNotAllowed: false,ForwardedByClientIP:    true,trees:                  make(methodTrees, 0, 9), // 路由树}engine.RouterGroup.engine = engine// 这里使用pool来池化Context,主要目的是减少频繁创建和销毁对象带来的内存分配和垃圾回收的开销engine.pool.New = func() interface{} {   return engine.allocateContext()}return engine
}

在执行完gin.Defualt后,gin的内容,里面已经默认初始化了2个handles

gineGet_69">1.2 Engine.Get

在Get方法内部,最终都是调用到了group.handle方法,包括其他的POST,DELETE等

  • group.handle
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {// 获取绝对路径, 将group中的地址和当前地址进行组合absolutePath := group.calculateAbsolutePath(relativePath)// 将group中的handles(Logger和Recovery)和当前的handles合并handlers = group.combineHandlers(handlers)   // 核心在这里,将handles添加到路由树中group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}
  • group.engine.addRoute
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {// 通过遍历trees中的内容,判断是否在这之前,同一个http方法下已经添加过路由// trees 是一个[]methodTree  切片,有2个字段,method 表示方法,root 表示当前节点,是一个node结构体root := engine.trees.get(method)  // 如果这是第一个路由则创建一个新的节点if root == nil {root = new(node)engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)
}
  • root.addRoute(path, handlers)

这里的代码比较多,其实大家可以简单的认为就是将handlepath进行判断,注意这里的path不是一个完整的注册api,而是去掉了公共前缀后的那部分字符串。

func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := path               // 存储当前节点的完整路径n.priority++                   // 优先级自增1numParams := countParams(path) // 统计当前节点的动态参数个数// non-empty tree,非空路由树if len(n.path) > 0 || len(n.children) > 0 {walk:for {// Update maxParams of the current node// 统计当前节点及子节点中最大数量的参数个数// maxParams 可以快速判断当前节点及其子树是否能匹配包含一定数量参数的路径,从而加速匹配过程。(GPT)if numParams > n.maxParams {n.maxParams = numParams}// Find the longest common prefix.// This also implies that the common prefix contains no ':' or '*'// since the existing key can't contain those chars.// 计算最长公共前缀i := 0max := min(len(path), len(n.path))for i < max && path[i] == n.path[i] {i++}// Split edge,当前路径和节点路径只有部分重叠,且存在分歧if i < len(n.path) {// 将后缀部分创建一个新的节点,作为当前节点的子节点。child := node{path:      n.path[i:],wildChild: n.wildChild,indices:   n.indices,children:  n.children,handlers:  n.handlers,priority:  n.priority - 1,}// Update maxParams (max of all children)// 路径分裂时,确保当前节点及子节点中是有最大的maxParamsfor i := range child.children {if child.children[i].maxParams > child.maxParams {child.maxParams = child.children[i].maxParams}}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = false}// Make new node a child of this nodeif i < len(path) {path = path[i:] // 获取当前节点中非公共前缀的部分// 当前节点是一个动态路径// TODO 还需要好好研究一下if n.wildChild {n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// Check if the wildcard matches// 确保新路径的动态部分与已有动态路径不会冲突。if len(path) >= len(n.path) && n.path == path[:len(n.path)] {// check for longer wildcard, e.g. :name and :namesif len(n.path) >= len(path) || path[len(n.path)] == '/' {continue walk}}panic("path segment '" + path +"' conflicts with existing wildcard '" + n.path +"' in path '" + fullPath + "'")}// 获取非公共前缀部分的第一个字符c := path[0]// slash after param// 当前节点是动态参数节点,且最后一个字符时/,同时当前节点还只有一个字节if n.nType == param && c == '/' && len(n.children) == 1 {n = n.children[0]n.priority++continue walk}// Check if a child with the next path byte existsfor i := 0; i < len(n.indices); i++ {if c == n.indices[i] {i = n.incrementChildPrio(i)n = n.children[i]continue walk}}// Otherwise insert itif c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65n.indices += string([]byte{c})child := &node{maxParams: numParams,}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) leafif n.handlers != nil {panic("handlers are already registered for path ''" + fullPath + "'")}n.handlers = handlers}return}} else { // Empty treen.insertChild(numParams, path, fullPath, handlers)n.nType = root}
}

gineRun_246">1.3 Engine.Run

func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()// 处理一下web的监听地址address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)// 最终还是使用http来启动了一个web服务err = http.ListenAndServe(address, engine)return
}

2. 路由查找

在上一篇文章中介绍了,http 的web部分的实现,http.ListenAndServe(address, engine) 在接收到请求后,最终会调用engineServeHTTP方法

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 从context池中获取一个Contextc := engine.pool.Get().(*Context)  // 对Context进行一些初始值操作,比如赋值w和reqc.writermem.reset(w)c.Request = reqc.reset()// 最终进入这个方法来处理请求engine.handleHTTPRequest(c)// 处理结束后将Conetxt放回池中,供下一次使用engine.pool.Put(c)
}

ginehandleHTTPRequest_285">2.1 engine.handleHTTPRequest()

func (engine *Engine) handleHTTPRequest(context *Context) {httpMethod := context.Request.Method  // 当前客户端请求的http 方法path := context.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 {root := t[i].root// Find route in treehandlers, params, tsr := root.getValue(path, context.Params)if handlers != nil {context.handlers = handlers  // 所有的handles 请求对象context.Params = params      // 路径参数,例如/api/user/:id , 此时id就是一个路径参数context.Next()  			 // 执行所有的handles方法context.writermem.WriteHeaderNow()return}}}// 这里客户端请求的地址没有匹配上,同时检测请求的方法有没有注册,若没有注册过则提供请求方法错误if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method != httpMethod {if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {context.handlers = engine.allNoMethodserveError(context, 405, default405Body)return}}}}// 路由地址没有找到context.handlers = engine.allNoRouteserveError(context, 404, default404Body)
}

2.2 context.Next()

这个方法在注册中间件的使用会使用的较为频繁

func (c *Context) Next() {// 初始化的时候index 是 -1c.index++s := int8(len(c.handlers))for ; c.index < s; c.index++ {c.handlers[c.index](c)  // 依次执行注册的handles}
}

我们来看一段gin 是执行中间件的流程

func TestGinMdls(t *testing.T) {engine := gin.Default()engine.Use(func(ctx *gin.Context) {fmt.Println("请求过来了")// 这里可以做一些横向操作,比如处理用户身份,cors等ctx.Next()fmt.Println("返回响应")})engine.GET("/index", func(context *gin.Context) {fmt.Println("index")})engine.Run()
}

curl http://127.0.0.1:8080/index

通过响应结果我们可以分析出,请求过来时,先执行了Use中注册的中间件,然后用户调用ctx.Next() 可以执行下一个handle,也就是用户注册的/index方法的handle


http://www.ppmy.cn/server/160255.html

相关文章

性能优化之动态加载

在过去近三十年的职业生涯里&#xff0c;有几年专注于运行时环境的开发与实现。在runtime中&#xff0c;动态加载技术是其中的基石之一。动态加载技术是指在系统运行过程中&#xff0c;根据需要把程序和数据从外存或网络加载到内存中的过程。其中&#xff0c;lazy loading&…

数据结构入门模板

一、栈&#xff08;Stack&#xff09; 定义 栈是一种**后进先出&#xff08;LIFO&#xff0c;Last In First Out&#xff09;**的数据结构。插入和删除操作只能在栈顶进行。 特点 只能从栈顶操作数据。操作简单&#xff0c;时间复杂度为 O ( 1 ) O(1) O(1)。 应用场景 表…

网络安全(渗透)

目录 名词解释 2、相互关系 3. 安全影响 名词解释 1、poc、exp、payload与shellcode POC&#xff08;Proof of Concept&#xff09;&#xff1a; 是一种概念验证代码或演示程序&#xff0c;用于证明漏洞的存在。 主要目的是通过简单的代码或操作向安全研究人员、开发人员…

WPS生成文件清单,超链接到工作簿文件-Excel易用宝

今天一大早&#xff0c;我们老板就心急火燎的来找到我&#xff0c;说这个文件夹中有很多工作簿&#xff0c;每次要打开一个文件都要打开这个文件夹&#xff0c;在密密麻麻的文件中查找&#xff0c;能不能在表格中做一个带超链接的列表&#xff0c;可以点击列表中的工作簿名称就…

[Bug]libGL.so.1: cannot open shared object file: No such file or directory

问题描述&#xff1a; 在服务器环境配置尝试导入 opencv (cv2) 模块时&#xff0c;系统找不到 libGL.so.1 这个共享库文件。这个问题通常出现在 Linux 系统中&#xff0c;特别是当系统缺少必要的图形库时。 (yolov11) python ./configs/yolov11/train.py Traceback (most rec…

OpenAI秘密重塑机器人军团: 实体AGI的崛起!

在人工智能的浪潮中&#xff0c;OpenAI一直是引领者的角色。这家以推进通用人工智能&#xff08;AGI&#xff09;为己任的公司&#xff0c;最近宣布了一项重大战略调整&#xff1a;重组其机器人部门&#xff0c;并计划推出实体AGI智能。这不仅是一次简单的组织架构变动&#xf…

如何在Mac上使用Brew更新Cursor应用程序

在这篇博文中&#xff0c;我们将介绍如何在Mac上更新Cursor应用程序&#xff0c;以及一些相关的使用技巧和功能。 什么是Cursor&#xff1f; Cursor是一款强大的工具&#xff0c;旨在帮助用户更好地编写、编辑和讨论代码。它结合了AI技术&#xff0c;使得编程过程更加高效和便…

抖音ip属地不准是什么原因?可以改吗

在数字化时代&#xff0c;社交媒体平台如抖音已成为人们日常生活的重要组成部分。随着各大平台对用户隐私和数据安全的日益重视&#xff0c;IP属地的显示功能应运而生。然而&#xff0c;不少抖音用户在使用过程中发现&#xff0c;显示的IP属地与实际位置存在偏差&#xff0c;这…