• gin源码实战 day2


    gin源码实战day2

    今天从中间件开始:
    中间件比较重要的代码昨天已经说了,就是next方法和它相关的终止调度器的方法。

    中间件

    注册中间件

    func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    	//Engine继承了RouterGroup,给当前Engine注册中间件。
    	engine.RouterGroup.Use(middleware...)
    	//这两行是调用方法,目的是再添加新的全局中间件后,重写构建处理404和405的内部处理链,因为新加入的中间件可能会影响到这些特殊情况的处理,所以需要重写调整。
    	engine.rebuild404Handlers()
    	engine.rebuild405Handlers()
    	return engine
    	//方法返回 Engine 实例本身。这样做允许链式调用,例如在创建 Engine 实例后连续添加多个中间件或定义路由。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Engine 结构体的 Use 方法的实现。Use 方法用于为整个应用添加全局中间件。中间件是一种特殊的函数,它会在处理 HTTP 请求的过程中被调用,可以用于日志记录、用户鉴权、数据处理等。
    解读:
    middleware …HandlerFunc: 可变参数,允许传入一个或多个中间件。HandlerFunc 是定义在 Gin 中的一个类型,代表请求处理函数或中间件的签名
    engine.RouterGroup.Use(middleware…): 这一行将传入的中间件添加到 Engine 内嵌的 RouterGroup 中。RouterGroup 是一个分组路由的概念,Engine 作为最顶层的分组,其实也有分组路由的能力。通过调用 RouterGroup.Use 方法,实现了中间件的注册。
    往里面再追一下:

    func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    	group.Handlers = append(group.Handlers, middleware...)
    	return group.returnObj()
    }
    
    • 1
    • 2
    • 3
    • 4

    这个显然就是把中间件(中间件本质是个特殊函数)添加进函数调用链种,中间件也会按添加时间先后顺序来执行。最后调用的这个方法是返回RouterGroup实例本身。
    如果看得懂就看这个解释,看不懂那就先把下面该了解的先了解

    先解读这个结构体:
    RouterGroup 结构体是 Gin Web 框架中的一个核心组件,用于分组路由和中间件。这个结构体允许将具有相同前缀的路由组织在一起,使得路由和中间件的管理变得更加方便和高效。下面是对 RouterGroup 结构体字段的详细解读:
    Handlers: 类型为 HandlersChain,这是一个 HandlerFunc 类型的切片这东西就是函数调用链。在 Gin 中,HandlerFunc 是处理 HTTP 请求的函数签名。Handlers 字段存储了应用于当前路由组的所有中间件。当处理到达该路由组的任一路由时,这些中间件将按照它们被添加的顺序被执行。

    basePath: 字符串类型,存储了当前路由组的基础路径(URL 前缀)。所有在该路由组中注册的路由都会自动加上这个前缀。例如,如果 basePath 是 /api,那么路由组中的一个路由 /users 实际上会被处理为 /api/users。

    engine: 指向 Engine 类型的指针。Engine 是 Gin 框架的核心结构,代表了整个 Web 应用。它负责管理路由、中间件以及启动 HTTP 服务器等。RouterGroup 通过 engine 字段与整个应用的上下文相关联,使得路由组可以访问应用级的设置和方法。

    root: 布尔类型,标识当前的 RouterGroup 是否是根路由组。在 Gin 中,最顶级的路由组(通常由 Engine 实例本身表示)被认为是根路由组。根路由组有一些特殊的行为,比如它可以直接访问和修改全局的中间件列表。

    然后针对这个结构体的方法可以说太多了:说几个常用的。
    Use(middleware …HandlerFunc) IRoutes
    添加中间件到当前路由组。这些中间件会被应用到路由组中的所有路由上。

    *Group(relativePath string, handlers …HandlerFunc) RouterGroup
    创建一个新的路由组,其路径是基于当前路由组的路径。可以为新的路由组添加特定的中间件。

    GET(relativePath string, handlers …HandlerFunc) 注册一个 GET 请求的路由,relativePath 是相对于路由组基础路径的相对路径。handlers 是处理该路由的函数列表。

    然后我继续看源码可以发现:
    RouterGroup是实现了两个相关的接口:IRouter和IRoutes。由于RouterGroup是路由组,而Engine也叫路由,所以Engine也实现了这两个接口,所以可以说这俩的方法差不多。

    我心里当时有个问题:实现这两个接口有什么意义
    还有这两个接口区别大吗?从源码可以看出,接口差别不是很大,而且Router是继承了Routes。

    实现 IRouter 接口的意义
    通过实现 IRouter 接口,RouterGroup 可以:

    1.支持分组路由:允许创建具有共同前缀的路由组,每个组可以有自己的中间件,这些中间件只对该组内的路由有效。这是组织大型应用路由的一种高效方式。

    2.链式调用:返回值类型通常设计为接口本身(如 IRoutes 或 IRouter),这支持了链式调用的编程风格,使得路由和中间件的设置更加流畅。

    3.保持接口一致性:无论是在顶级路由(由 Engine 实例管理)还是在路由组中添加路由和中间件,开发者都可以使用相同的方法。这降低了学习成本,并提高了代码的可读性和一致性。

    实现 IRoutes 接口的意义:
    1. 统一的路由配置接口
    IRoutes 接口为路由配置提供了一个统一的接口,使得开发者无论是在应用的顶层还是在路由分组中添加路由时,都能使用相同的方法。这样的设计不仅提高了代码的一致性,也使得路由的配置更加直观和易于理解。通过实现 IRoutes 接口,Gin 确保了不同层级的路由配置具有相同的操作方式和行为表现。

    2. 链式调用
    IRoutes 接口支持链式调用,这是一种流畅的编程风格,允许开发者通过连续调用方法来配置路由。这种方式简化了路由和中间件的添加过程,使得代码更加简洁和优雅。例如,你可以连续添加多个中间件或路由而不需要重复指定路由组或路由器实例:

    router.GET("/path", handler).POST("/path", postHandler).PUT("/path", putHandler)
    
    
    • 1
    • 2

    3. 灵活的路由和中间件管理
    通过实现 IRoutes 接口,Gin 能够提供灵活的路由和中间件管理能力。开发者可以轻松地为特定的路由或路由组添加中间件,控制中间件的作用范围,以及组织相关路由的逻辑结构。这对于构建大型和复杂的 Web 应用尤为重要,因为它允许将应用分解成更小、更易管理的部分。

    4. 支持 RESTful API 设计
    IRoutes 接口提供的方法支持 RESTful API 的设计原则,允许开发者根据不同的 HTTP 方法(如 GET、POST、PUT、DELETE)来定义路由处理函数。这有助于创建清晰、易于维护的 API 接口。

    5. 增强的可读性和可维护性
    实现 IRoutes 接口的路由配置方式增强了代码的可读性和可维护性。通过使用明确的方法名称(如 GET、POST)和支持链式调用,路由配置变得更加直观。此外,将路由和中间件的配置集中管理也使得维护更为方便,特别是在需要修改或扩展现有路由时。

    IRoutes: 方法返回 IRoutes 接口类型,这允许链式调用其他设置路由的方法。

    总结:
    Use 方法是 Gin 框架提供的一种方便的方式,用于为整个 Web 应用添加全局中间件。不仅可以对单个路由,还可以对路由组使用。极其方便。

    Engine对象初始化

    通过gin.New或gin.Default()可以初始化一个Engine对象,这通常是gin使用的第一行代码。
    gin.New()和gin.Default()的作用和区别:
    直接看源码:
    关于New

    func New() *Engine {
    	debugPrintWARNINGNew()
    	engine := &Engine{
    		RouterGroup: RouterGroup{
    			Handlers: nil,
    			basePath: "/",
    			root:     true,
    		},
    		FuncMap:                template.FuncMap{},
    		RedirectTrailingSlash:  true,
    		RedirectFixedPath:      false,
    		HandleMethodNotAllowed: false,
    		ForwardedByClientIP:    true,
    		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
    		TrustedPlatform:        defaultPlatform,
    		UseRawPath:             false,
    		RemoveExtraSlash:       false,
    		UnescapePathValues:     true,
    		MaxMultipartMemory:     defaultMultipartMemory,
    		trees:                  make(methodTrees, 0, 9),
    		delims:                 render.Delims{Left: "{{", Right: "}}"},
    		secureJSONPrefix:       "while(1);",
    		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
    		trustedCIDRs:           defaultTrustedCIDRs,
    	}
    	engine.RouterGroup.engine = engine
    	engine.pool.New = func() any {
    		return engine.allocateContext(engine.maxParams)
    	}
    	return engine
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    解读:
    从源码可以看出分为如下三部分:
    1.Engine的初始化。
    2.Engine 的 pool 字段初始化。

    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    
    
    • 1
    • 2
    • 3
    • 4

    初始化了Engine的sync.Pool,用于复用 Context 对象。sync.Pool 的 New 字段是一个函数,当从池中获取对象时,如果池为空,则调用此函数生成新的对象。这里,New 函数通过调用 allocateContext 方法为每个新请求分配一个新的 Context 实例,以提高性能并减少内存分配。
    3.函数返回,返回初始化后的Engine实例。

    总结
    gin.New 函数创建了一个新的 Gin Engine 实例,该实例通过各种默认设置预配置了路由处理和中间件管理的基本行为。这个实例还提供了一个 sync.Pool 用于 Context 对象的高效复用,以及一系列默认参数配置,旨在为开发者提供一个功能丰富且灵活的 Web 应用框架基础。

    关于Default

    func Default() *Engine {
        debugPrintWARNINGDefault()
        engine := New()
        engine.Use(Logger(), Recovery())
        return engine
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到是基于New,并且还通过use方法给Engine安装了两个中间件。

    总的来说:
    New就是创建了一个干净的Engine实例,而Default还预装了一个日志和恢复中间件的实例。可能对于大多数人来说Default更加的方便点。但是New为高级场景提供了更方便的使用。

    关于中间件的源码这里我也探索一下:
    这个是logger中间件的

    func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
    	//初始化日志格式化函数和输出目标
    	//formatter首先检查配置中是否提供了自定义的日志格式化函数。如果没有提供(即 nil),则使用默认的日志格式化函数 defaultLogFormatter。这个格式化函数负责将日志信息转换成特定的字符串格式。
    	formatter := conf.Formatter
    	if formatter == nil {
    		formatter = defaultLogFormatter
    	}
    	//out: 接着检查配置中是否指定了日志的输出目标。如果没有指定(即 nil),则使用默认的输出目标 DefaultWriter,通常是控制台(标准输出)。
    	out := conf.Output
    	if out == nil {
    		out = DefaultWriter
    	}
    	//这行代码从 conf(一个 LoggerConfig 类型的实例)中获取 SkipPaths 属性的值,并将其赋给本地变量 notlogged。SkipPaths 是一个字符串切片,包含了不应该记录日志的请求路径列表。作用:过滤日志记录:在日志中间件处理过程中,notlogged 用于指定哪些请求路径不需要被记录日志。这对于减少日志噪音、提高日志质量或者出于性能考虑跳过某些高频但不重要的路径的日志记录非常有用。
    	
    	notlogged := conf.SkipPaths
    	
    	//判断输出目标是否为终端
    	//这段代码用于检查日志的输出目标是否为终端,以便决定是否使用终端特有的格式化特性(如颜色)。首先假设输出目标是终端(isTerm := true),然后尝试将输出目标转换为 *os.File 类型,并使用 isatty 库检查文件描述符是否真的指向一个终端设备。如果不是,或者环境变量 TERM 被设置为 "dumb"(表示一个非交互式终端),isTerm 被设置为 false。
    	isTerm := true
    
    	if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
    		(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
    		isTerm = false
    	}
    
    	//初始化跳过日志记录的路径集合
    	//如果配置了不记录日志的路径列表(conf.SkipPaths),则创建一个映射(skip),将这些路径添加到映射中,以便快速检查一个路径是否应该跳过日志记录。
    	var skip map[string]struct{}
    
    	if length := len(notlogged); length > 0 {
    		skip = make(map[string]struct{}, length)
    
    		for _, path := range notlogged {
    			skip[path] = struct{}{}
    		}
    	}
    
    	//日志记录的 HandlerFunc
    	return func(c *Context) {
    		// Start timer
    		start := time.Now()
    		path := c.Request.URL.Path
    		raw := c.Request.URL.RawQuery
    
    		// Process request
    		c.Next()
    
    		// Log only when path is not being skipped
    		if _, ok := skip[path]; !ok {
    			param := LogFormatterParams{
    				Request: c.Request,
    				isTerm:  isTerm,
    				Keys:    c.Keys,
    			}
    
    			// Stop timer
    			param.TimeStamp = time.Now()
    			param.Latency = param.TimeStamp.Sub(start)
    
    			param.ClientIP = c.ClientIP()
    			param.Method = c.Request.Method
    			param.StatusCode = c.Writer.Status()
    			param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
    
    			param.BodySize = c.Writer.Size()
    
    			if raw != "" {
    				path = path + "?" + raw
    			}
    
    			param.Path = path
    
    			fmt.Fprint(out, formatter(param))
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    然后这个是Recovery中间件的实现

    func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
    	var logger *log.Logger
    	if out != nil {
    		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
    	}
    	return func(c *Context) {
    		defer func() {
    			if err := recover(); err != nil {
    				// Check for a broken connection, as it is not really a
    				// condition that warrants a panic stack trace.
    				var brokenPipe bool
    				if ne, ok := err.(*net.OpError); ok {
    					var se *os.SyscallError
    					if errors.As(ne, &se) {
    						seStr := strings.ToLower(se.Error())
    						if strings.Contains(seStr, "broken pipe") ||
    							strings.Contains(seStr, "connection reset by peer") {
    							brokenPipe = true
    						}
    					}
    				}
    				if logger != nil {
    					stack := stack(3)
    					httpRequest, _ := httputil.DumpRequest(c.Request, false)
    					headers := strings.Split(string(httpRequest), "\r\n")
    					for idx, header := range headers {
    						current := strings.Split(header, ":")
    						if current[0] == "Authorization" {
    							headers[idx] = current[0] + ": *"
    						}
    					}
    					headersToStr := strings.Join(headers, "\r\n")
    					if brokenPipe {
    						logger.Printf("%s\n%s%s", err, headersToStr, reset)
    					} else if IsDebugging() {
    						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
    							timeFormat(time.Now()), headersToStr, err, stack, reset)
    					} else {
    						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
    							timeFormat(time.Now()), err, stack, reset)
    					}
    				}
    				if brokenPipe {
    					// If the connection is dead, we can't write a status to it.
    					c.Error(err.(error)) //nolint: errcheck
    					c.Abort()
    				} else {
    					handle(c, err)
    				}
    			}
    		}()
    		c.Next()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    路由器组

    通过router.Group()创建的路由器组。追进去看看源码:

    func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    	return &RouterGroup{
    		Handlers: group.combineHandlers(handlers),
    		basePath: group.calculateAbsolutePath(relativePath),
    		engine:   group.engine,
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    解读:从这个代码来看你会发现它是RouterGroup的方法,那说明什么?
    说明这个创建路由组的方法是基于当前路由组创建一个新的子路由组。

    func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
    
    
    • 1
    • 2

    *group RouterGroup: 方法接收器,表示当前的路由组实例。新的路由组将基于这个实例创建。

    relativePath string: 新路由组的相对路径。这个路径会与父路由组的路径结合,形成新路由组的完整路径。

    handlers …HandlerFunc: 可变数量的 HandlerFunc,这些是中间件处理函数,将应用于新路由组中的所有路由。

    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Handlers: 使用 group.combineHandlers(handlers) 来合并当前路由组已有的中间件和新传入的中间件。这样,新路由组就继承了父路由组的中间件,同时也可以添加自己特有的中间件。这里再追源码:

    func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    	//计算最终的中间件列表大小,计算合并后的中间件列表的大小。它将当前路由组已有的中间件数量(len(group.Handlers))和新传入的中间件数量(len(handlers))相加。
    	finalSize := len(group.Handlers) + len(handlers)
    	//使用 assert1 函数检查合并后的中间件数量是否超过了 Gin 框架设定的上限(abortIndex)。如果超过了,会触发一个断言错误,提示“too many handlers”。这是为了防止因为中间件数量过多而引发的潜在问题。
    	assert1(finalSize < int(abortIndex), "too many handlers")
    	//make(HandlersChain, finalSize):根据计算出的最终大小创建一个新的 HandlersChain 切片,HandlersChain 是 HandlerFunc 类型的切片,用于存储中间件。
    	mergedHandlers := make(HandlersChain, finalSize)
    	//copy(mergedHandlers, group.Handlers):将当前路由组的中间件复制到新创建的 mergedHandlers 切片中。
    	copy(mergedHandlers, group.Handlers)
    	//copy(mergedHandlers[len(group.Handlers):], handlers):然后将新传入的中间件追加到 mergedHandlers 切片中,从 group.Handlers 之后的位置开始复制。这样,
    	copy(mergedHandlers[len(group.Handlers):], handlers)
    	//mergedHandlers 中首先包含了路由组原有的中间件,然后是新添加的中间件。
    	return mergedHandlers
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    basePath: 使用 group.calculateAbsolutePath(relativePath) 计算新路由组的绝对路径。这个方法将 relativePath 与父路由组的路径结合,确保新路由组的路径是基于父路由组的路径的扩展。这个函数的内部源码:

    func joinPaths(absolutePath, relativePath string) string {
    	//检查相对路径是否为空
    	if relativePath == "" {
    	//如果传入的相对路径 (relativePath) 为空字符串,那么没有必要进行任何连接操作,直接返回绝对路径 (absolutePath) 即可。
    		return absolutePath
    	}
    	//使用 path.Join 函数将绝对路径 (absolutePath) 和相对路径 (relativePath) 连接起来,生成一个新的路径 (finalPath)。path.Join 会自动处理路径分隔符,确保路径连接正确。
    	finalPath := path.Join(absolutePath, relativePath)
    
    	//首先,通过 lastChar 函数(这个函数的作用假设是获取路径字符串的最后一个字符)检查传入的相对路径的最后一个字符是否是斜线 ('/')。然后,同样使用 lastChar 函数检查连接后的最终路径的最后一个字符是否是斜线。如果相对路径的最后一个字符是斜线,但最终路径的最后一个字符不是斜线,说明在路径连接过程中丢失了尾部的斜线。在这种情况下,需要将斜线添加到 finalPath 的末尾,以保持路径的一致性。
    
    
    	if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
    		return finalPath + "/"
    	}
    	//如果相对路径不为空且不需要添加尾部斜线,或者已经按需添加了斜线,就返回最终的路径 (finalPath)。
    	return finalPath
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    engine: 新路由组继承了父路由组的 engine 字段。这是因为所有的路由组都是归属于同一个 Gin 引擎的,这样可以保证路由组能够访问到引擎级别的设置和方法。

    通过 Group 方法,Gin 框架提供了一种高效且灵活的路由组织方式,使得开发者可以轻松地为应用构建清晰、结构化的路由系统。

    从代码层面来说,创建路由组,仅是返回路由组的对象,路由组的本质是一个模板,使用路由器组添加路由,省去用户填写相同路径前缀和中间件的步骤。

    注册路由

    路由如何注册呢,和路由组又有什么关系?路由组本质只是一个模板,维护了路径前缀、中间件等信息,使用路由组添加路由,用户就省去了重复配置相同前缀和中间件的操作,所以通过路由组注册路由,和普通注册路由的本质是相同的。

    直接来看里面用到的这些请求都是注册路由。
    先说说什么是注册路由?
    注册路由是 Web 开发中的一个常见概念,指的是在 Web 应用或服务的路由系统中定义一条路由规则,以便于应用能够识别和响应特定的 HTTP 请求。在 Gin 框架中,注册路由的操作涉及到指定一个 HTTP 方法(如 GET、POST、PUT 等)、一个路径(URL 模式)以及一个处理该请求的函数(通常称为处理器或控制器)。当框架接收到一个 HTTP 请求时,它会根据请求的方法和路径找到对应的处理器并执行,以生成响应发送回客户端。

    注册路由的作用
    注册路由的主要目的是将特定的请求映射到对应的处理逻辑上。这样做有几个重要的好处:

    结构化的请求处理:通过为不同的请求路径和方法注册不同的处理器,可以清晰地组织你的应用逻辑,使得代码易于理解和维护。

    灵活性和扩展性:可以轻松添加或修改路由规则来满足应用的需求,无论是增加新的功能、改变现有功能的访问路径,还是引入新的中间件逻辑。

    明确的请求分发:框架根据注册的路由规则自动分发请求到对应的处理器,开发者无需手动解析请求路径,简化了请求处理流程。

    总结
    注册路由是 Web 应用开发的一个核心概念,它允许开发者定义如何响应不同的 HTTP 请求。在使用 Gin 这样的 Web 框架时,注册路由不仅能帮助你清晰地组织应用逻辑,还能提供灵活和强大的请求处理能力

    现在是源码解读
    GET

    func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    	return group.handle(http.MethodGet, relativePath, handlers)
    }
    
    func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    	//根据相对路径,计算绝对路径
    	absolutePath := group.calculateAbsolutePath(relativePath)
    	//合并处理器,实际上就是将handler追加到原有的处理器组切片中
    	handlers = group.combineHandlers(handlers)
    	//这个就比较重要,添加路由,涉及radix树添加结点方法。
    	group.engine.addRoute(httpMethod, absolutePath, handlers)
    	return group.returnObj()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    其他方法的都类似

    上下文

    注意gin和go的上下文不是一回事。
    gin上下文的方法分为几大类,创建、流程控制、错误关联、元数据管理,请求数据,响应渲染,内容协商。
    方法按内容进行编写,易于查询。

    type Context struct {
    	//请求对象
    	writermem responseWriter
    	//响应对象
    	Request   *http.Request
    	Writer    ResponseWriter
    
    	Params   Params
    	handlers HandlersChain
    	index    int8
    	fullPath string
    
    	engine       *Engine
    	params       *Params
    	skippedNodes *[]skippedNode
    
    	// 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
    
    	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
    	Errors errorMsgs
    
    	// Accepted defines a list of manually accepted formats for content negotiation.
    	Accepted []string
    
    	// queryCache caches the query result from c.Request.URL.Query().
    	queryCache url.Values
    
    	// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
    	// or PUT body parameters.
    	formCache url.Values
    
    	// SameSite allows a server to define a cookie attribute making it impossible for
    	// the browser to send this cookie along with cross-site requests.
    	sameSite http.SameSite
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这个是上下文的创建方法(初始化方法):

    func (c *Context) reset() {
    	c.Writer = &c.writermem
    	c.Params = c.Params[:0]
    	c.handlers = nil
    	c.index = -1
    
    	c.fullPath = ""
    	c.Keys = nil
    	c.Errors = c.Errors[:0]
    	c.Accepted = nil
    	c.queryCache = nil
    	c.formCache = nil
    	c.sameSite = 0
    	*c.params = (*c.params)[:0]
    	*c.skippedNodes = (*c.skippedNodes)[:0]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    //拷贝上下文,如果上下文被传递给协程,必须使用拷贝(副本拷贝)。

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    用于获取当前请求上下文中的最后一个处理器,在gin框架中,一个请求可能会通过多个处理器,这些处理器被组织成一个链式结构,存储在Context的handlers字段中。

    func (c *Context) Handler() HandlerFunc {
    	return c.handlers.Last()
    }
    
    • 1
    • 2
    • 3

    c.handlers.Last(): 调用 handlers 切片(HandlersChain 类型)的 Last 方法来获取切片中的最后一个元素,即当前请求上下文中的最后一个处理器。HandlersChain 是 HandlerFunc 类型的切片,用于按顺序存储处理当前请求的所有处理器。

    关于Error
    它用于在当前请求的上下文中记录一个错误

    func (c *Context) Error(err error) *Error {
    	//检查错误是否为 nil
    	//在开始处理之前,这段代码首先检查传入的错误 (err) 是否为 nil。如果是,那么会引发一个 panic,因为向上下文添加一个 nil 错误没有意义,且可能是编程错误。
    	if err == nil {
    		panic("err is nil")
    	}
    	
    	//尝试将错误转换为 *Error
    	//这里使用 Go 标准库的 errors.As 函数尝试将 err 转换(类型断言)为 *Error 类型。*Error 是 Gin 定义的一个错误类型,包含错误信息和错误类型(如 ErrorTypePrivate)。如果转换成功,ok 将为 true,并且 parsedError 将指向转换后的错误。
    	var parsedError *Error
    	ok := errors.As(err, &parsedError)
    	//处理无法转换的情况
    	//如果 err 不能被转换为 *Error 类型(即,它是 Go 的内置错误类型或其他自定义错误类型),这段代码会创建一个新的 *Error 实例,将传入的 err 作为其 Err 字段的值,并将错误类型设置为 ErrorTypePrivate。ErrorTypePrivate 是用来表示这是一个内部错误,通常不会被发送给客户端的。将错误添加到上下文的错误列表中
    	if !ok {
    		parsedError = &Error{
    			Err:  err,
    			Type: ErrorTypePrivate,
    		}
    	}
    	//将错误添加到上下文的错误列表中
    	//这里将处理后的错误(parsedError)添加到当前请求上下文的错误列表 (c.Errors) 中。c.Errors 是 *Error 类型的切片,用于存储请求处理过程中发生的所有错误。
    	c.Errors = append(c.Errors, parsedError)
    	//返回处理后的错误
    	return parsedError
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    总结
    Error 方法为 Gin 的请求上下文提供了一种机制,以统一的方式处理和记录请求处理过程中发生的错误。通过将错误记录在请求上下文中,开发者可以在请求的任何后续处理阶段访问和处理这些错误,例如在中间件或路由处理函数中。这样不仅有助于错误的集中管理,还能根据错误类型或内容定制响应。

    数据管理:
    用于设置键值对。

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个就是获取键值对

    func (c *Context) Get(key string) (value any, exists bool) {
    	c.mu.RLock()
    	defer c.mu.RUnlock()
    	value, exists = c.Keys[key]
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以直接看里面用很多*括起来的就表示不同的作用类型。

    看到这里我突然就理解怎么看源码了。其实了解大概的框架之后,剩下的内容全部变成了看方法。

    最后做一个重点总结
    1.上下文对象的创建,用到sync.pool来复用内存
    2.gin底层还是使用net/http包,gin的本质是一个路由处理器。
    3.每个请求方法都有一棵radix树
    4.添加中间件的过程就是切片添加元素的过程,也决定了中间件会按照添加时间的先后顺序来执行。
    5.可以为不同路由组添加不同的中间件。
    6.路由组本质只是一个模板,维护了路径前缀,中间件等信息,让用户省去重复配置相同前缀和中间件的操作。
    7.新路由器组继承父路由器组的所有处理器。
    8.如果上下文需要被并发使用,需要使用上下文副本。

  • 相关阅读:
    MySQL-操作数据库(存储引擎)
    DHCP基础
    Unity中Shader的屏幕坐标
    纯css手写switch
    Redis实践记录与总结
    软件工程毕业设计课题(27)基于JAVA毕业设计JAVA运动场地预约系统毕设作品项目
    CERLAB无人机自主框架: 2-动态目标检测与跟踪
    ES查询数据的时报错:circuit_breaking_exception[[parent] Data too large
    学习多线程,创建多线程的三种方式
    win32-注册表-项名长度-值得最大长度-注意事项
  • 原文地址:https://blog.csdn.net/TOMOT77/article/details/136181047