【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
无法处理请求,则将请求传递给下一层中间件MiddleWare2
,MiddleWare2
处理请求,处理完毕后,返回响应给上层调用者,再由上层调用者沿责任链继续将响应信息返回给上层调用者,直至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。构建完后返回给上层的调用者Handler
,Handler
将路由组对象组装好后,再将组装好的路由组对象返回给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.MaxInt8
是int8
类型能表示的最大整数,通常为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
。再实例化了Middleware1
、Middleware2
和FinalHandler
。之后使用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解决方案