【Gin】Gin框架性能优化:精进应用效率与稳定性的对象池策略(下)

12 篇文章 0 订阅

【Gin】Gin框架性能优化:精进应用效率与稳定性的对象池策略(下)

大家好 我是寸铁👊
【Gin】Gin框架性能优化:精进应用效率与稳定性的对象池策略(下)✨
喜欢的小伙伴可以点点关注 💝

在这里插入图片描述


前言

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

在现代的软件开发中,性能优化是一个至关重要的议题。特别是对于高负载的网络应用,如Web服务和API后端,有效地管理资源和提升响应速度至关重要。Gin框架作为一个轻量级的Go语言Web框架,以其高效的性能和灵活的特性受到了广泛的欢迎和采用。本文将聚焦于一种关键的性能优化技术——对象池模式的应用。
对象池模式通过重用已有的对象,避免了重复创建和销毁对象的开销,从而显著提升了应用程序的效率和性能。在Gin框架中,合理地利用对象池模式可以有效地管理资源,减少内存占用,并且降低GC(垃圾回收)的负担,进而提升系统的稳定性和可伸缩性。本文将深入探讨在Gin框架中如何实现和应用对象池模式,以及它带来的实际性能改进。


关键的类图和时序图

类图

对象池模式角色描述:
对象池(Object Pool)

对象池是主要的管理者,负责创建、维护和提供对象实例。它可以控制对象的数量、对象的生命周期管理,以及在需要时动态创建新的对象。

可复用对象(Reusable Object)

可复用对象是对象池中所管理的实际对象。它们被设计成可重复使用的状态,通过对象池的获取和释放机制,避免了频繁的对象创建和销毁,提高了系统的性能和效率。

客户端(Client)

客户端是使用对象池模式的主体。它们通过对象池获取需要的对象实例,并在使用完毕后将其归还。客户端不需要关心对象的具体创建和销毁过程,而是通过简单的获取和释放接口来管理对象的生命周期。

在这里插入图片描述

图对象池模式类图

由上图得:定义好对象池抽象类ObjectPool,声明好对象池容器,创建对象的几种方法getSlow()pin()New(),获取对象的方法Get()、放回对象的方法Put(),将对象放回对象池的方法addObject()ObjPool为具体的实现类,设置从ObjectPool中取出对象的索引,用于从对象池中取出索引,并具体实现创建对象的几种方法getSlow()pin()New(),将对象放回对象池的方法addObject()Reusable Object类聚合具体实现类,用于对该池进行各类对象的操作,有取出对象的方法GetObject(),放回对象的方法PutObject()、设置取出对象的索引SetIndex()ObjClient为真正使用的客户端,设置具体的索引和取出对象GetObject()、放回对象PutObject()、使用对象UseObject()


时序图

在这里插入图片描述

图 对象池模式时序图

由上图可得:客户端请求对象:客户端发起请求GetObjRequest,希望从对象池中获取一个对象GetObject()

对象池检查:对象池检查是否有空闲的对象可供分配。对象池分配对象:如果有可用对象,对象池分配一个对象给客户端ProvideObject()

客户端使用对象UseObject():客户端使用获取到的对象进行业务操作。客户端释放对象:客户端使用完对象后,将其释放回对象池PutObject()

对象池管理对象状态:对象池接收到释放的对象后,管理对象的状态,使其变为可复用状态,并添加到对象池中addObject(),以备下一次请求使用。


主程序的流程

由下图可得:程序一开始,客户端发起请求,请求从对象池ObjectPool中获取对象,对象池ObjectPool中判断ObjectPool是否有空闲的对象,有则从对象池ObjectPool获取Object空闲对象,无则调用创建对象的方法创建对象,返回创建好的对象Object给客户端使用,客户端使用对象,使用完毕后将对象归还到对象池中,对象池将归还的对象添加到对象池客户端使用中用于复用,程序结束。

如下图:
在这里插入图片描述

图 对象池模式主程序流程图

程序模块之间的调用关系

在这里插入图片描述

图 17 对象池GET获取对象源码深入剖析图


在这里插入图片描述

图 18 对象池PUT放回对象入池源码深入剖析图

对象池设计模式旨在通过重用对象来提高性能,尤其是在创建和销毁对象成本较高的情况下。在上图对对象池的GET获取对象和PUT放回对象的方法深度剖析后,以下是关于对象池设计模式涉及的主要角色和模块层次调用关系的描述:

主要角色:
对象池(Object Pool)
对象池是一个存储对象实例的集合,这些对象可以被重复使用而不需要反复创建和销毁。
通常,对象池在创建时会预先初始化一定数量的对象,并提供方法来获取对象和归还对象。


获取对象(GET)
通过对象池的 Get() 方法获取一个对象实例。通过 userPool.Get() 获取了一个 *User 对象。
如果对象池中有可用的对象实例,它会将一个空闲对象返回给调用者。


归还对象(PUT)
在使用完对象后,通过对象池的Put()方法将对象归还到对象池。在你的示例中,通过defer userPool.Put(user)来确保在函数结束时将 *User 对象归还给 userPool。对象池会在收到归还对象时,标记该对象为空闲状态,以便后续的 Get() 方法可以重新使用它。


模块层次调用关系描述:

由上图17、18可得:程序开始时,客户端代码在启动时会自动加载好对象池(通常是分配好一定的对象)。主要是根据Pool(池)类型创建所需要的对象池。对象池中通常包含几个成员:nocopy(不拷贝标识)、 local(表示本地池的指针)localSize(表示本地池的大小)、victim(表示受害区的指针)victimSize(表示受害区的大小), New()函数用于创建对象池中的对象。
之后,客户端调用对象池的Get()方法从对象池中取出所需类型的对象,Get()方法会调用pin()方法获取到对象池,再从对象池中的私有区中取出对象。如果私有区没有对象,则从对象池的共享区中取出对象。如果以上两个区都获取不到所需的对象,则需要调用getSlow(pid)方法,传入pid去其他进程的共享区中获取所需要的对象,如果获取不到,则前往本地池范围内的所有进程的受害区获取对象。最后,判断是否拿到对象,如果还是拿不到对象,并且已经定义对象的属性和成员,则调用本地的New()方法创建对象。涉及到更多细节Get()方法的代码深入剖析可见上图17。
最后,在涉及到整个业务处理完毕后,对象池注重对象的整个生命周期的管理,用完对象后,要把对象放回到对象池中。调用对象池的Put()方法讲用完的对象放回到对象池中。Put()方法先判断生成的随机数是否为0,为0则不放回对象,不为0则调用pin()方法从对象池中获得要放回对象池中的对象。再判断本地池的私有区是否为空,为空,则把对象放到私有区中。不为空则放到本地池的共享区中。涉及到更多细节Put()方法的代码深入剖析可见上图18。


在这里插入图片描述

图19 Gin框架对象池获取对象完整机制

由上图19可得:Gin框架对象池获取对象完整机制说明:调用GET方法先判断是否启用竞态检测机制,是的话则禁用竞态检测,避免并发操作时竞态检测影响性能,提高系统性能。不是的话则尝试获取本地池对象,先访问本地池地址看是否能获取本地池对象,如果能,则返回该本地池对象。如果不能获取对象,则进一步判断是否能从所有池中获取本地池对象,如果能,则返回该本地池对象。如果不能获取对象,则创建本地池对象并将原有的本地池的大小和地址进行拷贝。
获得本地池对象拥有:私有区(private)共享区(shared)。先访问本地私有区判断是否存在要获取的对象,存在则返回对象池的对象,不存在则再访问本地共享区判断是否存在要获取的对象。存在则返回对象池的对象,不存在则访问其他进程的共享区判断是否存在要获取的对象,存在则返回对象池的对象,不存在访问其他进程的受害区缓存的共享区判断是否存在要获取的对象,存在则返回对象池的对象,不存在则返回空对象。在获取对象池的对象后,解绑进程和处理器,重新启用竞态检测。判断对象池为空并且New对象不为空则调用New()创建一个对象池中的对象,最后返回对象池对象。


在这里插入图片描述

图 20 对象池本地区访问底层机制

对象池本地区访问底层机制:由上图20可得:先访问本地池的私有区(private),判断是否存在,如果存在要获取类型的对象则返回对象,如果不存在则访问共享区(shared),判断是否存在,如果存在要获取类型的对象则返回对象不存在,则先获取原进程的本地池的两个值:池的大小和池的指针(地址),根据池的大小(size)去遍历其他进程的本地池,遍历的机制:在size范围内,i依次从0size遍历,根据原进程的地址ptr,再加上(pid号再加1i 取模后的结果),从而获得其他进程的本地池的地址,进而访问其共享区,判断共享区是否存在要获取类型的对象。


在这里插入图片描述

图21 对象池受害者区访问底层机制

对象池受害者区访问底层机制:由上图21可得:先访问本地池受害者的私有区(private),判断是否存在,如果存在要获取类型的对象则返回对象,如果不存在则访问共享区(shared),判断是否存在,如果存在要获取类型的对象则返回对象不存在,则先获取原进程的本地池受害者区的两个值:池的大小和池的指针(地址),根据池的大小(size)去遍历所有进程的本地池,遍历的机制:在size范围内,i依次从0size遍历,根据原进程的地址ptr,再加上(pid号再加上 i 取模后的结果),从而获得其他进程的本地池的地址,进而访问其共享区,判断共享区是否存在要获取类型的对象。

下面是对象池模式整个调用过程的代码深入剖析:
对象池的代码如下:

在这里插入图片描述

图 22 对象池的池结构体代码
代码位置: pool.go文件的49-62行
noCopy
noCopy 是一个类型,通常用来避免结构体被复制。它可能是一个结构体,其定义在代码中未给出。它的存在可以防止 Pool 结构体在不应该被复制的情况下被复制,从而避免潜在的问题。


local localSize
local 是一个 unsafe.Pointer 类型的字段,指向一个固定大小的本地池(per-P pool)。实际上,它的类型应该是 [P]poolLocal,其中 [P] 表示某种固定大小的数组,每个元素类型为 poolLocal。unsafe.Pointer 类型表示这个指针可以指向任意类型的数据。
localSize 是一个 uintptr 类型的字段,表示 local 数组的大小。


victim victimSize
victim 是一个 unsafe.Pointer 类型的字段,指向上一个周期的本地池(local pool)。它保存了上一个周期中被剔除的缓存条目。
victimSize 是一个 uintptr 类型的字段,表示 victim 数组的大小。


New
New 是一个函数类型的字段,它用于在 Get 方法返回 nil 时生成一个新的值。函数签名是 func() any,表示它接受无参数并返回任意类型的值 any。注释指出,它不应该在调用 Get 方法的同时被并发修改。

Get()的代码如下:
在这里插入图片描述

图 23 对象池的Get()代码

代码位置:pool.go的127-154行
if race.Enabled { race.Disable() }:
这里假设 race 是一种竞争检测的机制(例如 Go 中的竞争检测工具),如果启用了竞争检测,则先禁用它,避免在并发访问时出现竞争条件。
l, pid := p.pin():
调用 pin() 方法来获取本地池的状态(l 是本地池的状态对象),以及当前的处理器标识符 pid。
x := l.private 和 l.private = nil:
尝试从本地池中获取一个私有对象 private,并将其置为空。这个 private 对象可能是预先分配给当前处理器的一个缓存对象。
if x == nil { ... }:
如果 x 是空的,尝试从本地共享池中弹出头部的对象。如果共享池中没有可用的对象,调用 getSlow(pid) 方法进行更复杂的获取过程。
runtime_procUnpin():
处理完成后,取消对本地池的固定(unpin),允许其他处理器再次访问该本地池。
if race.Enabled { race.Enable() ... }:
如果启用了竞争检测,则重新启用它,并且如果 x 不为空,则标记 x 对象为竞争关键地址。
if x == nil && p.New != nil { x = p.New() }:
如果 x 仍然为空,并且 Pool 结构体中定义了 New 函数(用来生成新对象),则调用 New 函数生成一个新的对象赋给 x。
return x:
返回获取到的对象 x,可能是从本地私有池、共享池或者通过 New() 函数新生成的对象。


在这里插入图片描述

图 24 对象池的Pin代码

代码位置:pool.go的198-210行
pid := runtime_procPin():
调用 runtime_procPin() 函数来固定当前处理器的标识符(pid)。这个函数可能会做一些操作,以确保后续操作在当前处理器上执行。
s := runtime_LoadAcquintptr(&p.localSize):
使用 runtime_LoadAcquintptr() 函数以加载-获取方式(load-acquire)从 p.localSize 加载数据。这种方式确保在读取 p.localSize 后,后续的操作可以看到一些关键的保证,比如确保在这个时间点上看到的数据至少和 p.localSize 一样大。
l := p.local:
加载 p.local 的值,使用加载-消费方式(load-consume)。这种加载方式适用于从多个线程或处理器访问的共享数据,以确保在读取 p.local 之后,所有被 p.local 引用的数据都是可见的。
if uintptr(pid) < s { ... }:
比较当前处理器的标识符 pid 是否小于 s。如果是,则返回 indexLocal(l, pid) 和 pid。indexLocal 可能是一个函数,用于根据给定的本地池和处理器标识符获取特定的本地池对象。
return p.pinSlow():
如果 pid 不小于 s,则调用 p.pinSlow() 方法。这表示需要执行一些更复杂或者耗时的操作来获取本地池对象。


在这里插入图片描述

图25 对象池的pinSlow()代码

代码位置:pool.go的212-234行
runtime_procUnpin():
调用 runtime_procUnpin() 函数取消当前处理器的固定(unpin)。这意味着当前处理器不再在某些操作上被锁定,可以执行其他操作。
allPoolsMu.Lock() 和 defer allPoolsMu.Unlock():
获取全局互斥锁 allPoolsMu,确保后续操作在多线程环境下安全进行。使用 defer 关键字确保在函数退出时释放锁。
pid := runtime_procPin():
重新固定当前处理器的标识符(pid),以确保后续操作在当前处理器上执行。
s := p.localSize 和 l := p.local:
加载 p.localSize 和 p.local 的值,准备进行后续的比较和操作。
if uintptr(pid) < s { ... }:
如果当前处理器的标识符小于 p.localSize,则返回 indexLocal(l, pid) 和 pid,表示在本地池中找到了合适的对象。
if p.local == nil { allPools = append(allPools, p) }:
如果 p.local 是空的,将当前对象池 p 添加到 allPools 切片中。这可能是在第一次初始化时执行。
size := runtime.GOMAXPROCS(0):
获取当前系统中允许的最大处理器数量(GOMAXPROCS),用于确定需要分配的本地池的大小。
local := make([]poolLocal, size):
使用 make 函数创建一个大小为 size 的 poolLocal 类型的切片 local,作为新的本地池。
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])):
使用原子操作将 p.local 设置为指向 local 切片的地址的第一个元素。这里使用了原子的存储操作,确保在多线程环境中修改 p.local 的安全性和一致性(store-release)。
runtime_StoreReluintptr(&p.localSize, uintptr(size)):
使用运行时的存储-释放操作将 p.localSize 设置为 size 的值。这也是为了确保在多线程环境中修改 p.localSize 的一致性。
return &local[pid], pid:
返回新分配的本地池中当前处理器标识符 pid 对应的 poolLocal 对象的地址和 pid。


在这里插入图片描述

图 26 对象池的getSlow()代码

代码位置:pool.go的156-190行
逐行分析:
size := runtime_LoadAcquintptr(&p.localSize):
使用 runtime_LoadAcquintptr 函数从 p.localSize 中以加载-获取(load-acquire)的方式加载大小。这种加载方式确保在读取 localSize 后,之后的读取操作不会乱序。
locals := p.local:
将 p.local 加载到 locals 变量中。这是一个加载-消耗(load-consume)操作,确保在读取 local 后,后续读取不会乱序。
for i := 0; i < int(size); i++ { ... }:
循环尝试从其他处理器的本地池中偷取一个元素。pid (pid+i+1)%int(size) 开始,尝试从其他处理器的本地池中获取对象。如果成功获取到,则返回该对象。
size = atomic.LoadUintptr(&p.victimSize):
使用原子操作加载 p.victimSize 的值。victimSize 存储了一个被盗用的对象缓存的大小。
if uintptr(pid) >= size { return nil }:
如果当前处理器标识符 pid 大于或等于 size,说明当前处理器没有被盗用的对象缓存,直接返回 nil。
locals = p.victim:
将 p.victim 加载到 locals 变量中,准备从被盗用的对象缓存中获取对象。
if x := l.private; x != nil { ... }:
尝试从当前处理器的私有对象缓存 l.private 中获取对象 x。如果成功获取到,则将 l.private 置为 nil,表示对象已被获取。
for i := 0; i < int(size); i++ { ... }:
如果从私有对象缓存未获取到对象,则循环尝试从被盗用的对象缓存中的共享部分获取对象。
atomic.StoreUintptr(&p.victimSize, 0):
将 p.victimSize 设置为 0,表示被盗用的对象缓存现在为空,以便后续获取操作不再考虑它。
return nil:
如果以上尝试都未成功获取对象,则返回 nil。

Put()代码如下:
在这里插入图片描述

图 27 对象池的Put()方法代码

代码位置:pool.go的95-117行
if x == nil { return }:
如果 x 是 nil,则直接返回,不进行任何操作。
if race.Enabled { ... }:
如果竞争检测功能 race 已启用,则执行以下操作:fastrandn(4) == 0 以概率 25% 的概率执行以下操作:随机丢弃对象 x。如果满足条件,直接返回,不将对象 x 放入池中。
race.ReleaseMerge(poolRaceAddr(x)):
发布竞争事件,通知竞争检测工具有关池中对象 x 的释放。
race.Disable():
禁用竞争检测。这段代码段内禁用竞争检测可能是为了避免在池操作期间的竞争检测干扰。
l, _ := p.pin():
调用 p.pin() 方法获取本地池中的一个空闲的 local 结构体 l。_ 是占位符,表示不关心 pin() 方法返回的第二个值。
if l.private == nil { l.private = x } else { l.shared.pushHead(x) }:
如果本地池 l 的私有部分 l.private 为空,则将对象 x 放入私有部分。否则,将对象 x 推入共享部分 l.shared 的头部。
runtime_procUnpin():
执行完操作后,解除对本地池的引用,释放可能的锁定或资源。
if race.Enabled { race.Enable() }:
如果竞争检测功能曾经启用过,则重新启用竞争检测。

总结: 这段Put代码实现了将对象 x 放入池中的逻辑。它首先检查对象是否为 nil,然后根据竞争检测的状态选择是否执行随机丢弃对象。接着通过 pin() 方法获取一个本地池的引用 l,并将对象 x 放入本地池的私有或共享部分中,最后释放池的引用并根据需要重新启用竞争检测。


案例及其调试分析

对象池模式通常通过以下方式来实现:
(1) 选择合适的数据结构:
对象池通常使用一个数据结构来存储对象的集合,如数组、链表或者更复杂的数据结构。这个集合用于存放已经创建并可重复使用的对象。


(2) 对象的创建:
在对象池初始化阶段,会预先创建一定数量的对象。这些对象会被放入对象池中,并处于可用状态。在需要时,对象池可以动态地创建新的对象。


(3) 对象的获取:
当有请求需要使用对象时,从对象池中获取一个对象。如果对象池中有空闲的对象,直接使用它们;如果没有,则根据需要创建新的对象。


(4) 对象的归还:
当对象使用完毕后,应该及时将对象归还到对象池中。这样做可以确保对象在未来的请求中可以被重复利用,而不是被销毁。


(5) 池的管理:
对象池通常包括对对象的管理,例如保证对象的并发安全性、设置对象的生命周期等。这些管理操作确保了对象在池中的有效使用和资源释放。


基于上述步骤,编写调试案例如下:
在这里插入图片描述

图88 定义的User结构体信息

User 结构体定义了用户的数据结构,包括 UsernameEmail字段。使用 json 标签指定了 JSON 序列化时的字段名,validate 标签定义了数据验证的规则。


在这里插入图片描述

图89 定义对象池

userPool 是一个对象池,使用sync.Pool实现。在这里,用于存储 User 对象,以便在需要时重复使用而不是频繁地创建和销毁。validate 是一个指向 validator.Validate 的指针,用于执行数据验证操作。


在这里插入图片描述

图90 注册User对象方法

registerUser 函数是处理/register POST请求的处理函数。user := userPool.Get().(*User) 从对象池获取一个 User 对象,并将其类型断言为 *User
defer userPool.Put(user) 在函数返回前,将User对象归还到对象池,确保对象在函数执行完毕后被释放或复用。


在这里插入图片描述

图91 绑定Json数据到User对象

c.ShouldBindJSON(user) 将请求中的 JSON 数据绑定到user对象上。如果绑定失败,将返回相应的错误信息。

在这里插入图片描述

图92 用户信息自定义验证

validate.Struct(user):这里使用了 validator 库对 user 对象进行数据验证。validate 是之前声明的 validator.Validate 实例,它具有方法用于执行各种数据验证操作。
err := validate.Struct(user):这一行代码调用了validator.ValidateStruct 方法来对user对象进行结构体级别的验证。如果验证失败,该方法会返回一个非空的 error 对象。if err != nil:如果 err 不为空,表示数据验证失败,这里会执行以下操作:
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}):使用 gin 框架的 JSON 方法返回一个 HTTP 400 Bad Request 状态码,并以 JSON 格式返回一个包含错误信息的响应体。err.Error() 返回具体的验证错误信息。
return:结束函数的执行,不再继续执行后续的注册逻辑。


在这里插入图片描述

图93 返回给客户端的响应信息

c.JSON(http.StatusOK, gin.H{"message": "User registered successfully", "user": user}):这行代码使用 gin 框架的 JSON 方法将一个 JSON 格式的响应发送给客户端。用户注册成功时,向客户端返回一个成功的 HTTP 响应,包含了成功消息和注册的用户信息,以便客户端可以得知注册操作已经完成。


运行调试案例,无任何报错信息,Gin引擎对象已经启动,等待来自客户端的Post注册请求。

在这里插入图片描述

图96 服务器监控成功

APIfox测试工具作为客户端,编写测试案例。以Json的格式编写要注册的用户名username和邮箱email,发送Post请求,根据验证规则如下图98:用户名不少于5位,邮箱要求为正式邮箱格式。
运行测试案例,发现测试不通过,debug发现错误信息,重新查看测试案例代码,重新测试,问题解决!

在这里插入图片描述

图97 APIfox验证用户信息测试
在这里插入图片描述

图98 自定义验证规则

重新运行调试案例,预期结果为:客户端用户注册验证通过,并收到来自服务端的验证成功的信息和注册的用户信息。验证失败则向客户端返回注册失败的信息。

在这里插入图片描述

图99 APIfox发起Post测试请求


在这里插入图片描述

图100 自定义检验规则

小结:这段代码结合了 gin 框架、对象池、数据验证等技术,用于实现一个简单的用户注册功能,通过对象池提高了对象的重复利用率,同时通过数据验证确保了用户输入数据的合法性。


对象池模式测试结果

服务端监听8080端口的register路由,并等待来自客户端的端口发送一条Post类型的Json请求信息,服务端对客户端的Json请求信息进行验证。
如下测试案例验证成功后,向客户端返回注册成功和注册的用户信息。"message": "User registered successfully","user": {"username": "George","email": "203885@qq.com" },对象池模式Demo测试成功!

在这里插入图片描述

图129 Apifox发起POST测试请求
在这里插入图片描述

图130 服务端监听信息

继续对对象池模式进行测试,设置客户端的请求信息中用户名位数小于4位,
验证失败,返回注册失败的错误信息。"error": "Key: 'User.Username' Error:Field validation for 'Username' failed on the 'min' tag",对象池模式Demo测试成功!并且整个测试的过程响应时间非常快,很好地体现了对象池模式的高复用的优势。


在这里插入图片描述

图131 Apifox发起POST测试请求

小结:通过对象池模式,可以重复利用已经存在的对象,避免频繁地创建和销毁对象,从而减少了垃圾回收的压力。测试对象池模式在处理 HTTP 请求时如何有效地管理对象的生命周期,提高了系统的性能和资源利用率。

结语

通过本文的探讨,我们深入了解了对象池模式在Gin框架中的应用与实现细节。对象池模式作为一种优化技术,不仅可以显著提升应用程序的性能,还能有效地管理系统资源,优化内存使用,并减少垃圾回收对系统性能的影响。在开发高性能和稳定性的网络应用时,合理地利用对象池模式是至关重要的。希望本文能为开发人员提供实用的指导和启发,使他们能够在实际项目中充分利用对象池模式,从而提升应用程序的效率和质量。


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


在这里插入图片描述

往期好文💕

保姆级教程

【保姆级教程】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稳定的有序遍历的方式

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寸 铁

感谢您的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值