欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 国际 > Go服务开发高手课(极客讲堂)

Go服务开发高手课(极客讲堂)

2025/9/25 20:37:49 来源:https://blog.csdn.net/san_77227487/article/details/146607175  浏览:    关键词:Go服务开发高手课(极客讲堂)

一、工具使用

1. pprof 工具。当 CPU 或者内存占用率过高时,使用 pprof 工具能够精准定位到消耗资源的热点代码

2. benchmark 工具。要对不同方案进行性能对比时,使用 benchmark 工具,可以获取不同方案在耗时内存消耗 方面的对比情况。

二、单机吞吐优化

1. 当用 map 构造集合时,可以将 value 类型设置为空结构体类型(struct{}),不占用内存空间,降低内存消耗。

2. 当创建 map 和切片对象时,如果可以提前确定容器容量,那就把容量传入 make 函数中,从而避免往集合中添加数据时触发扩容迁移,降低内存和 CPU 资源消耗。

3. 有大量字符串拼接操作时,可以使用 strings.Builder 类型,并利用它的内存预分配功能做字符串拼接。

4. 有大量整型转字符串操作时,可以用 strconv 库做转换,避免使用 fmt.Sprint 函数的反射和格式化。

5. 有大量字符串转字节切片操作时,可以用 unsafe 包,通过字符串和字节切片底层数组空间共用,实现高性能转换

6. 需要频繁创建相同类型的临时对象时,可以使用 sync.Pool 对象池,实现临时对象复用,从而减少 Go 中的内存分配和 GC 开销。

7. 需要频繁地创建协程,可以使用协程池。可以降低协程创建的开销。同时,协程池能限制同时运行的协程的最大数目,从而避免同时有太多协程,导致频繁进行协程调度。

六、并发等待:如何降低实时系统的响应延时?

1. WaitGroup 类型。一个规模较大的任务,为了提高执行效率,可以将其拆分成多个子任务,然后让这些子任务并发运行。而此时,如果还需要等待所有子任务都顺利执行完毕,那么 WaitGroup 类型能够精准地满足需求。

2. errgroup 包。errgroup 包可以看作是对 WaitGroup 类型的升级与封装。在实际开发中,需要周全地对可能出现的错误进行处理、灵活地取消任务以及精准地控制最大协程数等需求, errgroup 包就是最佳选择。

七、并发安全:如何为不同并发场景选择合适的锁?

1. 对数据的写操作较多或者读操作不频繁,可以使用互斥锁Mutex

2. 读操作远多于写操作,可以使用读写锁,允许多个协程同时进行读操作,提高并发读取的性能。

3. 当大量数据存储在 map 中,并且协程对 map 的访问相对均匀地分布在不同的键上时,可以考虑使用分段锁。具体是将 map 分成多段,每段有自己的锁,降低锁粒度,从而提升并发性能。

4. 需要对共享对象进行原子操作,可以利用 atomic 包无锁编程,避免加锁操作,从而提升性能。

八、并发map:百万数据本地缓存,如何降延时减毛刺?

1. sync.Map 借助两个 map 来达成读写分离的设计,提升读取操作的性能。

2. 然而,这种设计虽然效率高,但存在着不容忽视的问题:

(1) 需要两个map,内存占用相对较高。

(2) 数据修改频繁时,读表命中率低。不命中的时候,既要读一次读表,又要读一次写表,降低性能。

(3) 读命中率低时,还会产生两个 map 之间的数据拷贝开销。数据量大时,会导致较大的性能开销。

3. sync.Map 和普通 map 的对比:

    sync.Map通过‌读写分离、延迟更新、细粒度锁控制‌三项核心机制,在并发场景下实现比普通map更高的吞吐量‌。

a. 读写分离:读操作优先访问无锁的读map。

b. 延迟更新与删除优化:删除操作仅标记为逻辑删除,实际清理延迟到写map提升为读map时再批量处理,避免高频删除。

c. 细粒度锁控制:仅在读表未命中时,以及数据拷贝(读map拷贝为写map)时使用才互斥锁

4. 当 map 中缓存的数据比较多时,为了避免 GC 开销,可以将 map 中的 key-value 类型设计成非指针类型且大小不超过 128 字节,从而避免 GC 扫描。

九、网络编程:如何进行网络IO编程降消耗,提吞吐?

1. 网络 IO 模型有阻塞 IO非阻塞 IOIO 多路复用异步 IO 多种类型,实践中比较常用的是 IO 多路复用模型。

2. 阻塞 IO: socket 缓冲区没有准备好,让线程阻塞在 IO 调用不返回。

    每个连接我们都需要创建一个专门的线程来处理。

3. 非阻塞 IO:socket 缓冲区还没准备好,网络 IO 系统调用立即返回,不阻塞线程,线程可以处理另一个连接的请求。

    需要利用轮询不断做系统调用,浪费大量 CPU 资源。

4. IO 多路复用:一个线程阻塞监听多个连接的网络 IO 事件,当有连接的 socket 缓冲区准备好,IO 调用就会返回。

    既不用创建大量线程,也避免不断轮询。

5. 异步 IO:线程进行IO 调用会立即返回,由内核负责将 socket 缓冲区数据复制到用户空间,然后通知线程完成,整个过程完全没阻塞。

    各个平台对异步 I/O 的支持程度不一,这种方式使用起来复杂度较高,因此并不是很广泛。

6. epoll 技术解析

(1) epoll_create 函数,在 Linux 内核创建一个内核需要监听的网络连接池子。

(2) epoll_ctl 函数,增删改池子里需要监听的连接和事件。

(3) epoll_wait 函数,阻塞等待池子里连接的网络 IO 事件。

7. 线程调用 epoll_wait 方法时,接收来自操作系统的关于 网络 IO 事件就绪的通知 的触发模式

(1) 水平触发:

a. 只要socket 缓冲区有未处理数据,持续触发事件

b. 应用层未处理完数据时,内核保持fd在就绪队列

c. 下次epoll_wait()会立即返回该fd

d. 不需要保证缓冲区足够大。

(2) 边缘触发:

a. 仅在fd状态变化时触发一次通知

b. 内核不会保留fd在就绪队列(除非新事件到达)

c. 应用层必须一次性处理完所有数据。

d. 需要保证缓冲区足够大。

8. Golang 运行时是如何感知网络 IO 就绪事件:

(1) 在程序启动时,Golang 底层会单独启动一个线程,该线程每隔 10ms 轮询调用 netpoll 函数,尝试取出网络 IO 事件就绪的协程列表,进行唤醒操作。(非阻塞的轮询读取)

(2) 进行协程调度时,Golang 底层会为当前处理器寻找下一个可执行的协程。如果没有可调度的协程,就会尝试获取网络 IO 就绪的协程用于调度。

(3) GC 过程中调用完 STW(stop the world)后,都会调用 start the world,此时也会对网络 IO 事件就绪的协程进行唤醒操作。

十、网络通信:不改业务代码,如何降低延时?

1. 尽量不跨地域、不跨机房调用。

2. 响应速度要求高的上下游服务,部署到一台物理机里。

3. 将RPC 调用,合并编译成本地 SDK 的函数调用。

十一、数据库:怎么提升效率?

1. 读写分离。当数据库读 QPS 过高时,可以通过读写分离架构,增加从库来提升读 QPS。

2. 分表。当单表数据行数太多,导致读性能下降时,可以用分表架构,将一张表拆成多张小表,从而提升读性能。

3. 分库。当数据库写入 TPS 过高时,可以用分库架构,通过增加多个主库,分散单库的写压力,从而提高写 TPS 上限。

十二、分布式缓存:大促抢购,不知热点咋防热Key?

1. 本地缓存全量数据。要是数据量不大,可以直接在服务本地内存把所有数据都缓存起来,能大幅度降低热 Key 问题导致的 Redis 访问压力。

2. 本地只缓存热 Key 数据。当服务不能缓存全部数据时,可以接入热 Key 探测框架,只把那些被频繁访问的热 Key 数据存到本地。

3. Redis 读写分离架构。要是不想让业务层变得复杂,可以采取读写分离架构,给每个 Redis Server 都加上从库,让从库去应对热 Key 的高频率读取,分担压力。

4. Redis 提供的Proxy 热 Key 承载方案。利用 Proxy 来缓存热 Key 数据,承担热 Key 访问所带来的压力。比如 阿里云 Redis 的 Proxy Query Cache

十三、分布式缓存:大Key更新,拆分大Key如何防脏读?

1. 用 PB 序列化做数据压缩。在将数据存储到 Redis 时,不使用 JSON 格式,过用 PB 序列化,可以避免不必要的数据写入,从而有效减少数据体积。

2. 数据拆分,同时加上版本号作为key保证避免脏读。 将数据拆分成多个部分,并为每个部分添加版本号作为子 Key。然后重新组装时,根本版本号获得key值,再读书多个部分数据,组合成原数据。

十四、本地缓存:本地缓存全量数据的方案,需要解决的问题

1. 解决程序启动时的效率问题。

       数据量较小的情况,可以直接从数据库轮询获取数据。然而,面对大量数据时,这种方法会导致启动时间过长。为了加速程序启动,可以采用本地文件加载和数据库轮询加载相结合的策略。

2. 解决缓存更新的问题。

       对于对实时性要求不高的场景,可以设定一个时间间隔,定期从数据库轮询获取更新的数据。但是,对于对实时性要求高的场景,可以采用 RocketMQ 广播消费的方式,x更快速的数据同步。

3. 解决数据量过大的问题。

       当本地缓存的数据量超出单机内存的承载能力时,可以采用分片集群的思想。将数据分散加载到不同的服务集群中,从而降低单机内存的负担。

十五、项目拆分:业务逻辑复杂,如何拆分服务让协作清晰有序?

1. 确定是否有必要采用微服务架构。一方面要考虑项目的复杂程度;另一方面要考量公司的微服务基础设施是否完善。比如 快速编译、自动部署、Kubernetes容器、 服务注册和发现等。

2. 进行服务拆分时,可以从业务技术组织结构这三个维度入手。

十六、目录规范:几万行的大文件,如何重构目录结构?

经典的后端三层架构:

1. Controller 层。用于接收前端请求,并调用 Service 层。

2. Service 层。负责具体的业务逻辑处理,调用 DAO 层,不直接读写数据库。

3. DAO 层。负责 MySQL、Redis 等数据库的增删改查。

十七、设计原则和模式:功能持续迭代,如何减少改动?

1. 依赖反转原则:高层模块不应该直接依赖于低层模块的具体实现,两者都应该依赖于接口。

    作用:将高层模块和低层模块的具体实现解耦。

    适用场景:低层模块未来很可能需要更换或升级时。

2. 策略模式:对象的某个行为,在不同的场景中该行为有不同的实现算法。

    作用:消除过长的 if else 代码。(if else 只负责策略类型构造, 具体逻辑在各自策略类型里实现)

    适用场景:if else 各分支的功能一样,只是实现方法不一样时。

3. 简单工厂方法模式:根据传入的参数,动态决定应该创建哪一个结构体的实例。(这些结构体实现了同一个接口)

    作用:便干对象创建的集中管理,避免ifelse创建对象的逻辑在各处重复

    适用场景:需要根据不同的条件,创建实现相同接口的不同结构体时。

4. 建造者模式和函数选项模式:构建对象时,用统一的构建接口。但具体怎么构建,在构建接口以外的代码去灵活定义,比如通过一个函数去定义,并传入到构建对象接口。

作用:降低构建接口的复杂性,和修改维护的成本。

适用场景:构建函数需要有多种方式,构建函数参数也有多少方式。

5. 责任链模式:将一个复杂的处理过程,分散到多个处理对象中。 每个对象只处理自己特定的逻辑和职责,并且根据情况决定是否将请求传递给下一个对象。

作用:降低了系统的复杂性;实现处理逻辑在多个场景中的代码复用。

十八、函数设计:重复编写相似函数,怎样实现逻辑复用?

1. 反射机制。 reflect 的反射功能,可以将接口类型 interface{} 的变量转换为反射类型对象 reflect.Type 和 reflect.Value,通过它们就可以访问和操作真实对象的方法和属性。

    但在实际开发中,‌若类型已知,优先用类型断言‌;若类型需动态才能确定,则需使用反射。

2. 泛型功能。能够通过类型参数与类型约束,来构建泛型类型和函数。运用类型参数,让代码得以适配多种不同类型,无需为每种类型编写特定实现。

十九、代码陷阱:最易导致程序出错的四类代码坑

1. 接口变量判空问题。接口变量在底层会储存类型 和值 两个元素。当值为  nil  但类型不为  nil  时,就会出现判空不符合预期的情况。返回nil必须返回字面值nil,不能返回(nil值的)接口变量。

2. 循环变量的使用。在 Go 1.22 版本之前,循环迭代变量的作用域涵盖整个循环体。如果我们在循环内部,通过闭包的方式直接使用这个变量,极有可能引发错误。可以创建一个新变量,并以参数的方式输入。

3. 数值类型的 JSON 反序列化问题。当我们采用  map[string]interface{}  类型对 JSON 字符串进行反序列化操作时, int  类型会变成了  float64  类型。

4. WaitGroup使用不当。必须在启动协程之前,在外部调用 Add  方法。

   channel  使用不当。接收者存在超时逻辑时,使用的channel 应采用带缓冲区 (非阻塞型), 避免因接收者超时,而导致发送者的协程永远被阻塞(channel发送信号但无接收者)。

二十二、超时和重试:如何提升高并发重要请求的成功率?

1. 上游的超时时间,应大于下游超时时间 乘以 重试次数。

2. 重试阈值熔断, 当在一个时间窗口内,重试请求达到一定比例,就不再进行重试,避免大面积重试把下游打垮。

二十三、熔断和降级:下游服务大量报错,如何快速止损?

1. 让报错服务返回兜底数据。

2. 每个服务都事先预备好兜底数据机制。

二十四、限流:不用Redis,如何搞定高并发低延时服务限流?

保障服务的稳定性比即时性更重要,所以可以对服务做限流操作,并使用特定的限流算法。

从简单到复杂依次是:

1. 固定窗口算法。每段时间能处理的请求数是固定的。

    缺点:当请求处于窗口边界时,不能有效实现限流。

2. 滑动窗口算法。把每个固定窗口,切分成多个子窗口,再滑动地把子窗口组合成大窗口,再做限流。

    缺点:应对流量波动时,抗抖动能力差的问题依然存在。

3. 漏桶算法。处理请求的速度,在一段时间内被被限定在一个固定值,超过固定值之后,请求需要等待,或者直接拒绝。

    缺点:未能分挖掘系统资源的潜力。

4. 令牌算法。

(1) 系统会以固定的速率生成令牌,系统空闲时,令牌持续堆积。

(2) 出现大流量请求时,大量消耗令牌。令牌消耗完之后,请求需要等待,或者直接拒绝。

(3) 继续以固定的速率生成令牌。

好处:通过令牌总数限制系统繁忙程度的上限, 当系统资源利用已达峰值时,保持较低令牌的生产速度,让系统获得喘息的机会,避免“雪上加霜”。 既充分发挥系统潜力,也避免直接把系统打挂。

二十五:灰度发布:新功能上线如何有效控风险?

1. 蓝绿部署:先更新一部分实例,确认没问题之后,再更新剩余实例。

2. 灰度部署:先跟新个别实例,确认没问题之后,再更新所有。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词