【Gin】深度解析:在Gin框架中优化应用程序流程的责任链设计模式(下)

news2024/11/26 5:33:45

【Gin】深度解析:在Gin框架中优化应用程序流程的责任链设计模式(下)

大家好 我是寸铁👊
【Gin】深度解析:在Gin框架中优化应用程序流程的责任链设计模式(下)✨
喜欢的小伙伴可以点点关注 💝

在这里插入图片描述


前言

本次文章分为上下两部分,上部分为对理论的介绍,下部分为具体的底层代码深度剖析和编程实践,感兴趣的伙伴不要错过哦~

责任链设计模式作为一种经典的行为设计模式,在现代软件开发中扮演着重要角色。特别是在高效的Web应用开发中,如Gin框架这样的轻量级Go语言Web框架,合理地应用责任链模式可以显著提升代码的可扩展性和灵活性。本文将深入探讨在Gin框架中责任链模式的实现原理、优化策略以及实际应用场景。
责任链模式通过将请求的发送者和接收者解耦,将多个对象连成一条链,并在链上传递请求,直到有对象处理该请求为止。在Gin框架中,利用责任链模式可以有效地处理请求的流程控制、中间件管理和异常处理,使得代码结构更加清晰和可维护。本文将探索如何在Gin框架中设计和优化责任链模式,以提升应用程序的性能和可维护性。


关键的类图和时序图

(1)类图
责任链模式包含以下几个主要角色:
Handler(抽象处理者):
声明一个处理请求的接口,通常包含一个指向下一个处理者的引用。

MiddlerWare(具体处理者):
Gin的中间件实现抽象处理者的处理方法,并根据自身的处理能力决定是否处理请求,如果不能处理则将请求传递给下一个处理者。

Client(客户端):
创建一个具体处理者对象的链,并向链上的第一个处理者对象发送请求。


在这里插入图片描述

图 28 责任链模式类图

由类图28可得:先声明一个处理请求的接口Handler,通常聚合一个指向下一个处理者的引用successor。声明处理请求的方法handlerRequest(),由具体的中间件责任方MiddleWare实现。接着Gin的中间件实现抽象处理者的处理方法,调用handlerRequest()方法根据自身的处理能力决定是否处理请求,如果不能处理则将请求传递给下一个处理者nextHandler。客户端创建一个具体处理者对象的链,并向链上的第一个处理者对象发送请求。


(2)时序图

在这里插入图片描述

图 29 责任链模式时序图

由上图29可得:客户端发送请求:客户端调用具体处理者链的第一个处理者的handleRequest()方法,将请求request传递给链的起始点MiddleWare1
责任链中的处理者Midlleware处理请求:每个具体中间件处理者Midlleware收到请求后,根据自己的处理能力决定是否处理请求。
请求传递到合适的处理者:如果当前处理者能够处理请求,则处理完成;如果不能处理,则将请求传递给链中的下一个处理者Midlleware
链的末端处理:请求request会沿着链依次传递,直到被处理或者到达链的末端。如果末端处理者能够处理请求processing Request,则处理完成后,沿着责任链返回处理完后的响应信息response`。如果整个链上的处理者都不能处理请求,则请求最终未被处理。


主程序的流程

由下图30可知:一开始,Client客户端构建责任链后,开始发起请求,责任链的中间件MiddleWare1开始接收请求,然后处理请求,处理完毕后,返回响应给上层调用者,如果客户端接收到响应后,则程序退出。
接着如果责任链的中间件MiddleWare1无法处理请求,则将请求传递给下一层中间件MiddleWare2MiddleWare2处理请求,处理完毕后,返回响应给上层调用者,再由上层调用者沿责任链继续将响应信息返回给上层调用者,直至Client客户端。
如果责任链的中间件MiddleWare2无法处理请求,则将请求传递给下一层中间件MiddleWare……MiddleWare……处理请求,处理完毕后,返回响应给上层调用者,再由上层调用者沿责任链继续将响应信息返回给上层调用者,直至Client客户端。
如果责任链的中间件都无法处理请求,则将请求传递给最后一层中间件FinalMiddleWare处理请求,处理完毕后,返回响应给上层调用者,再由上层调用者沿责任链继续将响应信息返回给上层调用者,直至Client客户端。如果最后一层中间件FinalMiddleWare都无法处理请求,则责任链程序结束退出。

在这里插入图片描述

图 30 责任链模式主程序流程图

程序模块之间的调用关系

下图为程序各模块之间的调用关系:

在这里插入图片描述

图 31 责任链模式程序模块调用图
由上图31可知,责任链模式主要有如下3个角色:
(1) IRoutes(处理者):
定义处理请求的接口,通常包含一个处理方法(handle),其具体实现方(ConcreteHandler)实现处理请求的方式。每个处理者对象中通常会包含一个指向下一个处理者的引用(后继者)。


(2) CombineHandlers(具体处理者):
实现 IRoutes接口,处理请求的具体逻辑。如果自己能够处理请求,则处理;否则将请求传递给下一个处理者。当然,也可以设置拦截请求,将请求只在本层进行处理,不传递给下一层责任方进行处理。


(3) Client(客户端):
负责创建和提交请求对象。按照指定的顺序构建责任链,可以设置拦截器,用于拦截请求,最后将请求发送到链的起始点。


下面是对上图各层次调用关系的描述:

客户端调用r.GET进行按照指定的顺序构建责任链,GET方法实现IRoutes接口,GET方法将构建路由组对象的请求转发给真正的构建路由组对象的handle方法,该handle方法实现IRoutes接口。handle方法进一步转发构建GET中指定顺序的中间件责任链请求给具体处理者CombineHandlers,具体处理者CombineHandlers负责按照客户端指定的顺序构建中间件责任链。责任链中的每一个处理者存在Next()Abort()方法,调用Next()方法遍历责任链中的请求者对象,依次将请求传递给链中的具体处理者对象业务图见下图32。Abort()方法设置索引为超出合法范围的值,使得不将请求转发给下一个处理者,实现拦截效果,业务图见下图33。构建完后返回给上层的调用者HandlerHandler将路由组对象组装好后,再将组装好的路由组对象返回给GET方法,GET方法返回嵌入责任处理链的路由组对象给客户端(责任链的起始点)进行调用,客户端拿到对象后,做下一步的处理。

在这里插入图片描述

图 32 责任链Next处理业务图

在这里插入图片描述

图 33 责任链Abort拦截业务图

在上图的基础上,下面对各个模块的代码进行深入剖析:
在这里插入图片描述

图 34 责任链客户端代码

gin.Default() 创建了一个默认的 Gin 路由引擎实例 r。
r.GET("", middleWare1, middleWare2, Home) 定义了一个 GET 请求的路由,其中 middleWare1 middleWare2 是中间件函数,用于在请求到达最终处理函数 Home 之前执行预处理或者其他操作。
r.Run(":8080") 启动了 HTTP 服务器,监听在 8080 端口上。

在这里插入图片描述

图 35 客户端指定处理请求的中间件代码

函数签名和参数:
func middleWare1(c *gin.Context):这是一个函数签名,接收一个 *gin.Context 类型的参数 c,用于处理 HTTP 请求和响应。
处理逻辑:fmt.Println("M1 请求部分"):这行代码输出 “M1 请求部分”,表示这是中间件处理请求的部分,可以在这里执行一些预处理逻辑,如日志记录、权限检查等。
c.Next()
c.Next() 是 Gin 框架中用于将控制传递给链中的下一个处理程序的方法。在这里,它表示将控制权传递给下一个注册的中间件或路由处理函数。fmt.Println(“M1 响应部分”):这行代码输出 “M1 响应部分”,表示中间件处理请求后的响应部分,可以在这里执行一些后续处理逻辑,如记录响应时间、清理资源等。
注释的代码 c.Abort()
//c.Abort():这是一个被注释掉的代码片段。在 Gin 框架中,如果调用 c.Abort(),它将会中止当前请求链的执行,不会再继续执行后续的中间件或路由处理函数。如果取消注释,将会导致请求处理过程被中止,不再执行后续的处理函数。

执行流程:

当有一个HTTP请求到达与该中间件关联的路由时,这段代码的执行流程如下:当请求进入时,中间件输出 “M1 请求部分”,执行一些请求前的逻辑。
c.Next() 调用将控制权传递给下一个中间件或路由处理函数。如果注释掉的 c.Abort() 被取消注释,则请求的处理将在此中间件结束,不再继续向下执行。
如果没有调用 c.Abort() 或者注释掉了,请求将继续执行下一个中间件或者最终的路由处理函数。当控制返回给该中间件时(表示后续处理完成或者中间件链中断),输出 “M1 响应部分”,执行一些请求后的逻辑。


在这里插入图片描述

图 36 IRoutes接口

代码位置:RouterGroup.go的33-51行
方法解析:
Use
Use(...HandlerFunc) IRoutes
作用:注册一个或多个中间件函数,这些中间件函数会在后续的路由处理中被调用。返回值:返回 IRoutes 接口本身,以支持链式调用。
HTTP 方法相关路由
这些方法 (Handle, Any, GET, POST, DELETE, PATCH, PUT, OPTIONS, HEAD) 都接受路由路径作为第一个参数,后跟一个或多个 HandlerFunc,表示路由处理函数。每个方法都允许注册对应 HTTP 方法的路由处理器。
返回值:同样返回 IRoutes 接口,支持链式调用,以便在代码中可以连续调用多个路由注册方法。
Match
Match([]string, string, ...HandlerFunc) IRoutes
参数:第一个参数是 HTTP 方法的列表,第二个参数是路由路径模式,后跟一个或多个 HandlerFunc
作用:注册一个支持多种 HTTP 方法的路由处理器。
返回值:返回 IRoutes 接口。
静态文件服务相关方法:
StaticFile(string, string) IRoutes:注册单个静态文件的路由处理器。
StaticFileFS(string, string, http.FileSystem) IRoutes:注册单个静态文件的路由处理器,并指定文件系统。
Static(string, string) IRoutes:注册指定路径下所有文件的静态文件服务。
StaticFS(string, http.FileSystem) IRoutes:注册指定路径下所有文件的静态文件服务,并指定文件系统。

总结:
这个接口定义了一组方法,用于在一个路由处理器中注册路由和处理函数。通过IRoutes接口,可以方便地添加中间件、处理各种 HTTP 方法的请求,以及处理静态文件服务。这种设计使得路由的注册和处理能够保持清晰和模块化,符合常见的 Web 框架的路由管理模式。


在这里插入图片描述

图 37 GET方法代码

代码位置:routergroup.go的116-118行

函数签名:
GET(relativePath string, handlers ...HandlerFunc):这是一个方法定义,属于RouterGroup类型的接收者 group。它接收一个相对路径 relativePath 和一个或多个HandlerFunc类型的处理函数作为参数。
IRoutes 是一个接口,用于表示路由集合或路由的操作。
功能说明:
GET方法作为 RouterGroup 的一个方法,是为了注册一个处理 GET 请求的路由。
relativePath string 参数表示注册的路由的相对路径,如 “/”、“/users” 等。
handlers ...HandlerFunc 参数是一个变长参数,接收一个或多个 HandlerFunc 函数,用来处理请求。
方法调用:
roup.handle(http.MethodGet, relativePath, handlers):在 GET 方法内部,调用了 group.handle 方法,将 HTTP 方法名 “GET”、相对路径 relativePath handlers 函数传递给它。
http.MethodGet 是 Go 标准库中定义的常量,表示 HTTP GET 请求方法。
返回值:
return group.handle(...):GET 方法返回了group.handle(...)的结果。通常情况下,这个方法会返回路由集合或者支持路由操作的接口,以便可以进一步链式调用其他路由相关的方法。
执行流程:
在 Gin 框架中,RouterGroup 类型的 GET 方法是一个便捷方法,它内部调用了 group.handle 方法来处理注册 GET 请求的路由。具体的处理流程如下:当调用 GET 方法注册路由时,实际上是通过 group.handle 方法进行注册。group.handle 方法会将 “GET”、relativePath handlers 作为参数传递给底层的路由处理器进行处理和注册。这样做的好处是可以通过不同的 HTTP 方法(例如 GET, POST, PUT 等)来注册不同的路由处理逻辑,同时保持了代码的简洁性和可读性。


在这里插入图片描述

图 38 具体处理者代码

代码位置:routergroup.go的86-91行

函数签名:
handle(httpMethod, relativePath string, handlers HandlersChain):这是一个方法定义,属于 RouterGroup 类型的接收者 group。它接收三个参数:
httpMethod:表示 HTTP 请求方法,如 "GET""POST" 等。
relativePath:表示路由的相对路径,例如 “/”、“/users” 等。
handlers HandlersChain:是一个类型为 HandlersChain 的参数,表示一系列的处理函数链。
路径计算和处理器组合:
calculateAbsolutePath 方法确保生成正确的完整路径,考虑了路由组的前缀等因素。combineHandlers 方法可能用来将当前路由组的中间件与传入的处理函数链合并,确保请求能够按照正确的顺序执行。
路由注册:
addRoute 方法将最终确定的HTTP方法、路径和处理函数链注册到 Gin 框架的路由引擎中,以便后续能够根据请求的HTTP方法和路径找到对应的处理函数。
返回值:
returnObj 方法返回当前路由组的某个接口或对象,用于可能的链式调用或其他路由相关操作。
这段代码展示了 Gin 框架中路由注册的核心逻辑。它负责计算路由的绝对路径,合并处理函数链,最终将路由信息注册到底层的路由引擎中。通过这种设计,框架能够支持灵活的路由定义和中间件处理,同时保证了性能和可扩展性。


在这里插入图片描述

图 39 具体处理者真正构建责任链代码
代码位置:routergroup.go的241-248行

函数签名:
combineHandlers(handlers HandlersChain) HandlersChain:这是一个方法定义,属于RouterGroup类型的接收者group。它接收一个类型为 HandlersChain 的参数 handlers,表示一系列的处理函数链。返回类型为 HandlersChain,即处理函数链。
参数解释:
handlers HandlersChain:表示要合并到当前路由组 (group) 的处理函数链。HandlersChain 可能是一个类型为 []func(c *Context) 的切片,用于存储中间件和处理函数。
计算最终大小:
finalSize := len(group.Handlers) + len(handlers):计算合并后的处理函数链的总长度。group.Handlers 是当前路由组已有的处理函数链的长度,handlers 是传入的新的处理函数链的长度。
断言检查:
assert1(finalSize < int(abortIndex), "too many handlers"):使用 assert1 函数来断言 finalSize 必须小于 abortIndex,否则会输出 “too many handlers” 的错误信息。这是为了确保合并后的处理函数链不会超过某个预设的最大限制,避免潜在的内存溢出或其他问题。
创建合并后的处理函数链:
mergedHandlers := make(HandlersChain, finalSize):根据 finalSize 创建一个新的 HandlersChain,即 mergedHandlers,用于存储合并后的处理函数链。
复制处理函数:
copy(mergedHandlers, group.Handlers):将当前路由组 (group) 的已有处理函数链复制到 mergedHandlers ``的开头部分。 copy(mergedHandlers[len(group.Handlers):], handlers):将传入的新处理函数链 handlers 复制到 mergedHandlers 的末尾部分,从 group.Handlers 的长度位置开始复制。
返回合并后的处理函数链:
return mergedHandlers:返回合并后的 HandlersChain,即包含了当前路由组的处理函数链和传入的新处理函数链的完整链条。
处理函数链合并:
combineHandlers 方法用于将当前路由组的已有处理函数链与传入的新处理函数链进行合并,确保请求按照正确的顺序执行所有中间件和处理函数。
长度和断言检查:
在合并前,通过计算和断言来确保合并后的处理函数链不会过长,以保证系统的稳定性和性能。
返回值:
返回合并后的处理函数链,以便在路由注册时使用。
这段代码展示了 Gin 框架中如何处理路由组的处理函数链的合并逻辑。通过这种方式,框架能够支持在路由组中动态添加中间件和处理函数,保证了灵活性和可扩展性。


在这里插入图片描述

图 40 责任链的数据结构
代码位置:gin.go的54行

定义:定义了一个HandlerFunc请求处理者的切片,用于存储HandlerFunc请求处理者,构建责任链。


在这里插入图片描述

图 41 请求处理者的定义
代码位置:gin.go的48行

定义:定义HandlerFunc类型,用于创建具体请求处理对象。


在这里插入图片描述

图 42 责任链访问下一个责任方Next()代码
代码位置:context.go的182-188行

方法说明:
c.index 是 Context 结构体中的一个字段,用于跟踪当前执行的中间件或处理函数的位置。
c.handlers 是一个存储 HandlerFunc 的切片,这些函数是注册到当前路由处理器的中间件和处理函数。
c.index++ 将 index 递增,以准备执行下一个中间件或处理函数。
for 循环用来遍历 handlers 中的函数,从 index 所指的位置开始执行,直到数组末尾或者中途某个函数决定中断执行。
执行流程:
调用Next()方法会使 index 递增,从而将控制权交给下一个注册的中间件或处理函数。
每次循环,通过 c.handlers[c.index](c) 调用 index 所指的函数,并将当前的 Context 对象 c 传递给它。
循环继续,直到 index 超过了 handlers 的长度或者某个中间件函数调用了 Next() 以停止后续执行。


在这里插入图片描述

图43 定义拦截索引
代码位置:context.go的50-51行

abortIndex 是一个 int8 类型的常量。
math.MaxInt8int8 类型能表示的最大整数,通常为 127
>> 1 是位运算操作,表示将 math.MaxInt8 右移一位,即将其值除以 2,得到的结果约为 63(实际值取决于具体的整数大小和运算系统)。
这样设定的目的是使abortIndex成为一个比较大的数值,足以确保 c.index 大于等于 abortIndex 后可以立即停止后续的处理函数调用。

在这里插入图片描述

图 44 请求处理者设置拦截器
代码位置:context.go的199-201行
代码解释:
c.index Context 结构体中的一个字段,用于跟踪当前执行的中间件或处理函数的位置。
abortIndex 是一个常量或全局变量,用于表示中止处理流程的索引值。
Abort() 方法将 c.index 设置为 abortIndex,这样在接下来调用 Next() 方法时,循环将直接结束,不再执行后续的处理函数或中间件。


在这里插入图片描述

图 45 判断请求处理者是否设置拦截
代码位置:context.go的191-193行

代码解释:
c.index Context 结构体中的一个字段,用于跟踪当前执行的中间件或处理函数的位置。
abortIndex 是一个常量或全局变量,用于表示中止(abort)处理流程的索引值。
IsAborted() 方法通过比较 c.index 是否大于或等于abortIndex来判断当前处理流程是否已经被中止。
如果c.index大于或等于 abortIndex,则返回 true,表示当前处理已被中止;否则返回 false,表示未被中止。


责任链模式案例及调试分析

责任链模式案例编写如下:
下面分析一下每个部分的功能和调用流程:
结构定义和接口:
在这里插入图片描述

图101 定义Handler接口
Handler 接口:
Handle(c *gin.Context) 方法用于处理请求。
SetNext(handler Handler) 方法用于设置下一个责任链节点。


在这里插入图片描述

图102 定义Middleware中间件

Middleware 结构体:实现了 Handler 接口。


在这里插入图片描述

图103 定义中间件1的Handle方法

Handle(c *gin.Context) 方法中,打印 “M1 接收请求”,然后调用下一个处理者(如果存在),最后打印 “M1 得到响应”。


在这里插入图片描述

图104 定义中间件2的Handle方法

Handle(c *gin.Context) 方法中,打印 “M2 接收请求”,然后调用下一个处理者(如果存在),最后打印 “M2 得到响应”。


在这里插入图片描述

图105 定义末端中间件的Handle方法
FinalHandler 结构体:作为最终的处理者,实现了 Handler 接口。
Handle(c *gin.Context) 方法中,打印 “FinalHandler 接收请求”,然后调用具体的业务逻辑函数 Home(c),最后打印 “FinalHandler 得到响应”。

在这里插入图片描述

图106 定义视图层代码
Home视图层部分:

fmt.Println("Home Receiving……") 放在 Home 函数中的最开始,这样在请求到达时会立即打印 “Home Receiving……”。
c.String(200, "Home Receiving……") 在完成日志记录后立即向客户端发送 “Home Receiving……” 响应
责任链的构建和运行:


在这里插入图片描述

图107 客户端构建责任链

解读: 在 main 函数中:先创建了 Gin 引擎实例 r。再实例化了 Middleware1Middleware2FinalHandler。之后使用 SetNext 方法将它们串联起来,形成责任链:middleware1 -> middleware2 -> finalHandler。将middleware1.Handle方法作为 Gin 路由处理函数注册到了根路径 “”,这意味着当收到 GET 请求时,责任链会依次处理该请求。最后通过 r.Run(":8080") 启动 Gin 服务器,监听在 8080 端口上。
小结:责任链模式案例展示了如何使用 Go 和 Gin 框架构建一个简单的责任链,用于处理 HTTP 请求。每个中间件和最终处理者都负责一部分逻辑,并通过 SetNext 方法连接成链条,确保请求依次经过每个处理者,并且每个处理者都能在适当的时机打印日志和处理响应。
调试分析:
在执行时,假设收到一个 GET 请求:
Gin 路由会将该请求交给 middleware1.Handle 处理。
middleware1.Handle 中会打印 “M1 接收请求”,然后调用 middleware2.Handle。
middleware2.Handle 中会打印 “M2 接收请求”,然后调用 finalHandler.Handle。
finalHandler.Handle 中会打印 “FinalHandler 接收请求”,然后调用 Home(c) 处理实际的业务逻辑。
Home(c) 会在控制台打印 “Home Receiving……”,并向客户端返回 “Home Receiving……” 字符串。
控制流会逆序返回,最终 finalHandler.Handle 打印 “FinalHandler 得到响应”,然后依次是 middleware2.Handle 和 middleware1.Handle 的响应打印。
Gin引擎对象启动成功,代码无报错,责任链构建成功,正在监听8080端口,Demo启动成功!具体输出结果见测试结果部分。


在这里插入图片描述

图 108 成功启动责任链模式案例


责任链模式测试结果

APIfox测试工具监听向8080端口发送GET请求:显示Home Reciving……,说明责任链构建成功,且将信息Home Reciving……正确显示到客户端。
在这里插入图片描述

图132 Apifox发起GET测试请求


责任链模式测试结果进一步剖析如下:
在这里插入图片描述

图133 责任链模式测试结果剖析图


客户端构建责任链并发起监听8080端口请求,接下来分析控制台输出的顺序是否与调试分析的预测一致:
在这里插入图片描述

图134 Apifox发起GET请求
访问 http://localhost:8080/ ,向服务端发出一条GET请求,可以看到以下控制台输出:

(1) 请求发出:
M1 接收请求:Middleware1 接收请求。与分析图的序号①对应
M2 接收请求:Middleware2 接收请求。与分析图的序号②对应
FinalHandler 接收请求:FinalHandler 接收请求。与分析图的序号③对应
Home视图层 Home Receiving……:Home 函数处理请求,输出 “Home Receiving……”。与分析图的序号④对应
同时向客户端(APIfox发起的请求端)发送"Home Receiving……


(2) 接收响应:
FinalHandler 得到响应:FinalHandler 处理完请求,得到响应。与分析图的序号⑤对应
M2 得到响应:Middleware2 得到最终处理结果的响应。与分析图的序号⑥对应
M1 得到响应:Middleware1 得到 Middleware2 的响应,最终完成整个请求的处理。与分析图的序号⑦对应。
综上,无论是控制台输出的结果还是客户端显示的信息,都与整个调试分析的结果一致,责任链构建成功,测试通过!


在这里插入图片描述

图135服务端监听端口多次测试请求

持续发起多次请求,责任链也能够正常处理,不发生请求,则持续监听端口。
再测试一下拦截请求的效果,这里只需要将调用下一个处理者代码替换为c.Abort()即可,这样就能将请求拦截在Middleware2处理者,而
不会传给下一层的具体处理者。


在这里插入图片描述

图 136 进行请求的拦截

预期结果如下:
由于在Middleware2这一层拦截掉,请求不会转发给下一层处理,即最后处理者和Home业务函数的内容都不会输出,客户端也不会显示出Home Receiving…… 输出结果预期如下:
M1 接收请求 M2 接收请求 M2 得到响应 M1 得到响应
测试结果如下:
客户端发起请求,没有接收到Home发送的内容,测试结果与预期一致!


在这里插入图片描述

图137 客户端发起GET请求

再看一下控制台输出的内容:只输出M1和M2两个中间件有关的内容,无输出最后处理者的内容,测试结果与预期一致!

在这里插入图片描述

图138 服务端监听端口输出信息

结语

责任链模式作为一种优秀的设计模式,在Gin框架中展现了其强大的灵活性和可扩展性。通过本文的探讨,我们深入理解了责任链模式在处理请求流程、中间件管理和异常处理方面的应用。合理地利用责任链模式可以使代码更加模块化和可复用,从而提高了应用程序的设计质量和开发效率。希望本文能够为开发者提供实用的指导和启发,帮助他们在实际项目中充分发挥责任链模式的优势,构建更加健壮和高效的Web应用程序。


看到这里的小伙伴,恭喜你又掌握了一个技能👊
希望大家能取得胜利,坚持就是胜利💪
我是寸铁!我们下期再见💕


在这里插入图片描述

往期好文💕

保姆级教程

【保姆级教程】Windows11下go-zero的etcd安装与初步使用

【保姆级教程】Windows11安装go-zero代码生成工具goctl、protoc、go-zero

【Go-Zero】手把手带你在goland中创建api文件并设置高亮


报错解决

【Go-Zero】Error: user.api 27:9 syntax error: expected ‘:‘ | ‘IDENT‘ | ‘INT‘, got ‘(‘ 报错解决方案及api路由注意事项

【Go-Zero】Error: only one service expected goctl一键转换生成rpc服务错误解决方案

【Go-Zero】【error】 failed to initialize database, got error Error 1045 (28000):报错解决方案

【Go-Zero】Error 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using password: YES)报错解决方案

【Go-Zero】type mismatch for field “Auth.AccessSecret“, expect “string“, actual “number“报错解决方案

【Go-Zero】Error: user.api 30:2 syntax error: expected ‘)‘ | ‘KEY‘, got ‘IDENT‘报错解决方案

【Go-Zero】Windows启动rpc服务报错panic:context deadline exceeded解决方案


Go面试向

【Go面试向】defer与time.sleep初探

【Go面试向】defer与return的执行顺序初探

【Go面试向】Go程序的执行顺序

【Go面试向】rune和byte类型的认识与使用

【Go面试向】实现map稳定的有序遍历的方式

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数学建模--微分方程

目录 常见的微分方程模型 微分方程建模的基本步骤 代码示例 常微分方程 ​编辑 ​编辑 偏微分方程 ​编辑 应用实例 结论 如何在数学建模中准确识别和选择合适的微分方程模型&#xff1f; 微分方程模型在解决实际问题中的应用案例有哪些&#xff1f; 常微分方程&a…

SpringBoot整合FFmpeg进行视频分片上传

SpringBoot整合FFmpeg进行视频分片上传------>Windows 分片上传的核心思路&#xff1a; 将文件按一定的分割规则&#xff08;静态或动态设定&#xff0c;如手动设置20M为一个分片&#xff09;&#xff0c;用slice分割成多个数据块。为每个文件生成一个唯一标识Key&#xf…

ONNX模型的量化

我们都希望从代码中榨取更多的性能&#xff0c;对吧&#xff1f; 在现代&#xff0c;充斥着需要大量计算资源的复杂机器学习算法&#xff0c;因此&#xff0c;榨取每一点性能至关重要。 传统上&#xff0c;机器学习算法是在具有支持大量并行计算能力的 GPU 上进行训练的。但是…

WordPress建站:如何使用ChemiCloud搭建外贸独立站

以前自行搭建一个网站&#xff0c;不懂一点技术那是很难完成的&#xff0c;现如今WordPress的出现极大地降低了搭建网站的技术门槛&#xff0c;不需要懂任何代码&#xff0c;只需按步骤操作就行。WordPress 是一个非常流行的开源内容管理系统&#xff08;CMS&#xff09;&#…

职业教育计算机网络综合实验实训室建设应用案例

近年来&#xff0c;职业教育在培养技能型人才方面发挥着越来越重要的作用。然而&#xff0c;传统的计算机网络技术教学模式往往重理论、轻实践&#xff0c;导致学生缺乏实际操作能力和职业竞争力。为了改变这一现状&#xff0c;唯众结合职业教育特点&#xff0c;提出了“教、学…

Kubeflow v1.7.0 创建新用户

文章目录 为新用户创建配置文件配置用户密码重启auth生效 为新用户创建配置文件 apiVersion: kubeflow.org/v1beta1 kind: Profile metadata:name: kubeflow-cyw-example-com # replace with the name of profile you want, this will be users namespace name spec:owner:k…

STC单片机UART映射printf

文章目录 使用STC-ISP生成UART初始化函数 增加如下函数&#xff0c;注意使用printf函数需要添加 #include <stdio.h> 头文件 #include <stdio.h>void Uart1_Init(void) //9600bps12.000MHz {SCON 0x50; //8位数据,可变波特率AUXR | 0x01; //串口1选择定时器2为…

【Spring】——Spring概述、IOC、IOC创建对象的方式、Spring配置、依赖注入(DI)以及自动装配知识

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大二学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…

LeetCode 101.对称二叉树 C写法

LeetCode 101.对称二叉树 C写法 思路&#xff1a; 将该树一分为二&#xff0c;左子树的左边与右子树的右边比&#xff0c;左子树的右边与右子树的左边比&#xff0c;不相等或者一边为空则不是对称。 代码&#x1f50e;&#xff1a; bool _isSymmetric(struct TreeNode* Leftroo…

程序员开发指南

在这个快节奏的时代&#xff0c;作为一名程序员&#xff0c;大家都希望能更快地开发出高质量的应用&#xff0c;而不是花费大量时间在基础设施和后台服务的搭建上。今天&#xff0c;我要向大家介绍一款专为懒人开发者准备的一站式开发应用的神器——MemFire Cloud。 一站式开发…

使用代理访问内网:实验二

目录 环境搭建 内网搭建&#xff08;win2019&#xff09; 跳板机搭建&#xff08;win10&#xff09; 实验步骤 1. win10上线kali 2. 借助msf做代理 3. 在攻击机上做个代理&#xff0c;访问目标网站 4. 使用SocksCap64工具&#xff0c;进行sock4a隧道的连接 5. 启用soc…

TypeScript 的主要特点和重要作用

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

最短路(dijkstra迪杰斯特拉)

最短路径问题在图论中是一个经典的问题&#xff0c;目的是找到从一个起始顶点到其他所有顶点的最短路径。Dijkstra算法是解决非负权图最短路径问题的常用算法。下面是一个使用Dijkstra算法解决最短路径问题的Java程序例子。 动画描述(从0节点开始更新) 问题描述 假设有一个图…

【机器学习西瓜书学习笔记——模型评估与选择】

机器学习西瓜书学习笔记【第二章】 第二章 模型评估与选择2.1训练误差和测试误差错误率误差 欠拟合和过拟合2.2评估方法留出法交叉验证法自助法 2.3性能度量查准率、查全率与F1查准率查全率F1 P-R曲线ROC与AUCROCAUC 代价敏感错误率与代价曲线代价曲线 2.4比较检验假设检验&…

VSCode+Vue3无法找到模块“../components/xxxxx.vue”的声明文件的错误

莫名奇妙的错误 今天用Vue3写个demo&#xff0c;在components下面新建了一个DeviceList.Vue的文件&#xff0c;在HomeView引用它后居然报错&#xff0c;提示&#xff1a;无法找到模块“…/components/DeviceList.vue”的声明文件&#xff0c;真是离了个大谱&#xff0c;文件明…

【Redis】 拓展:Redis - BigKey方案探讨

BigKey: 用户越多&#xff0c;redis数据越多&#xff0c;bigkey会使得缓存数据更大&#xff0c;网络带宽会被占用&#xff0c;执行效率就低下&#xff0c;高并发的时候吞吐量QPS也会下降。 产生原因&#xff1a; 看如下list&#xff1a; 一个key的内容太大&#xff0c;比如1M&…

VR舒适度术语表与检查表:为MR和空间计算应用创业者准备

随着混合现实&#xff08;MR&#xff09;和空间计算应用的发展&#xff0c;确保用户在虚拟环境中的舒适度变得尤为重要。本文将介绍一套专门针对VR舒适度的术语表&#xff0c;并提供两个知名VR游戏作为示例&#xff0c;来展示如何应用这些术语。这些术语和示例可以帮助开发者更…

基于Hadoop的服装电商数据分析系统【Hdfs、flume、HIve、sqoop、MySQL、echarts】

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍总体研究方向数据集介绍配置flume文件HIve建表HIveSQL大数据分析MySQL建表Sqoop命令导出数据到MySQL数据可视化店铺销售情况.......等 总结每文一语 有需要本项目的代码或文档以及全…

【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(十四)-租云服务器及配环境、docker基本命令

主要介绍了租云服务器和docker配置、基本命令&#xff01;&#xff01;&#xff01; 文章目录 前言 一、云平台 二、租云服务器及安装docker 1.阿里云 2.安装docker 三、docker命令 将当前用户添加到docker用户组 镜像&#xff08;images&#xff09; 容器(container) 四、实战…

Vue3-18 组件基础

组件的定义 我们在项目的src/components种定义组件&#xff0c;vue提供了几种简单的组件的定义方式 在单文件种定义组件 <script setup> import { ref } from vue const count ref(0) </script> <template><div>第一种<button click"count…