Go语言上手-工程实践 | 青训营笔记

92 阅读12分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记.

并发与并行

image.png 1. 并发 2.并行 并行可以理解成并发的一个手段。

image.png 协程创建和调度由Go语言本身去完成,比线程轻量很多。一次可以创建上万左右的协程,这也就是Go语言可以高并发的原因所在。

1.1 Goroutine

image.png 协程的使用实例,go语言中开启一个协程只需要在调用函数时在函数前面加关键字go。time.Sleep主要是为了保证在子协程在执行完之前,主协程不退出。但这里有两个方式,后期将协程同步的时候再来看。

可以看到输出是乱序的,也就是说HelloGoRoutine是通过并行去打印输出的。

1.2 CSP (Communicating Sequential Processes)

协程之间的通信:

image.png 提倡通过通信共享内存。 Goroutine是程序并发的执行体,Channel通道相当于把协程做了一个连接,就像顺序队列,遵循先入先出,能保证收发数据的顺序。 通道Chanel就是让一个Goroutine发出特定的值到另一个Goroutine的一个通信机制。

Go其实也保存着通过共享内存实现通信的机制。通过共享内存实现通信必须是通过互斥量对内存进行一个加锁,也就是需要获取临界区的一个权限。在一定程度上会影响程序的性能。

1.3 Channel

通过make关键字创建:

image.png

根据是否有缓冲区大小,分为有缓冲通道和无缓冲通道两种。 无缓冲通道会导致发送的foroutine和接收的goroutine同步化,因此无缓冲通道也被称为同步通道。 解决同步问题的方式就是使用有缓冲区的通道。其实也是一个典型的生产-消费模型,缓冲区满了就会阻塞直到有元素被取走。

channel的使用:

image.png A用for循环遍历,B用range遍历src中的数据,这里其实通过src这个channel实现了A协程和B协程的通信。 最终主协程通过range操作遍历dest channel,这里用打印来代替可能会出现的复杂操作。 这里可以看到通过src和dest channel的传递,是能保证顺序性的(并发安全)。 这里dest是一个缓冲区大小3的队列,主要考虑的点在于消费者的消费速度,就是说它的逻辑可能会比打印更复杂,消费者消费速度可能会慢一些,生产者的逻辑可能比较简单、生产速度快,用带缓冲的队列就不会因为消费者的消费速度问题影响生产者的效率。缓冲区可以解决执行效率问题。

可以看到每个channel都用defer做了个延迟的资源关闭。

注意Go测试的运行语句:

image.png

1.4 并发安全 Lock

image.png

Go也保留着通过共享内存实现通信的机制,会存在多个goroutine同时操作一块内存资源的情况。 加锁方式通过Lock获取临界区资源,withoutlock没有共享资源保护。

用sleep实现道路的阻塞。但不是优雅的,因为我们不知道子协程确切的执行时间,因此无法精确地设置wait的Sleep时间.

1.5 WaitGroup

Sync包下的waitGroup,主要是为了实现并发安全操作和协程同步

image.png

Go语言可以使用WaitGroup实现并发任务的同步,其实是内部维护了一个计数器,计时器的值可以增加或减少,n个并发任务计数器+n。每个任务完成时通过调用Done方法将计时器-1,最后调用Wait()方法进行阻塞等待所有并发任务执行完。

image-20220510202453483

优化前面讲过的↑这个

image-20220510202547761

最后通过wait进行阻塞

image-20220510202702574

2 依赖管理

2.1 依赖

各种开发包,学会站在巨人的肩膀上image-20220510202905256

framework\collection等一系列的依赖都可以通过SDK引入

image-20220510203320987

依赖管理的方案,现在广泛应用的是Go Module

2.1.1 GOPATH

image-20220510203355283

是Go语言支持的一个环境变量,go项目的工作区

弊端:image-20220510204215682

加入本地有两个项目,pkg有两个不同版本,因为两个项目依赖的都是同一个src的源码,所以A和B不能同时构建成功。

下一阶段:

2.1.2 Go Vendor

image-20220510204531934

当前项目依赖的副本,项目的依赖优先从vendor目录下进行获取,vendor目录下没有再到gopath寻找。

前面的例子,如果A项目的vendor下是v1版本、B项目的vendor下是v2版本,这样就能保证project1和2都能构建成功。

image-20220510204859230

通过Vendor的管理模式不能很好地控制V1和V2的版本选择问题。不能清晰地标识依赖版本的概念。

为了解决Vendor这个问题:

2.2 Go Module

image-20220510204957424

从1.1试验性引入,1.6默认开启

image-20220510205036858

1、能有一个文件描述依赖哪些包,然后包如何去唯一地定位,这个其实对应go里的go.mod文件

2、有中心仓库去管理依赖库,对应go.mod里是Proxy

3、在go.mod里主要涉及两个工具:go get/mod

如果熟悉JAVA可以类比JAVA里的maven。

下面针对前面讲的三要素,对go.mod里每个元素的规则和配置进行详细讲解

image-20220510205440085

最上面一行:模块路径,标识了一个模块,如果是github代表是托管的github。如果每个包想单独被引用的话在每个package下都需要建一个go.mod文件。

model path 和 版本号 唯一定位

版本规则:

image-20220510205729855

MAJOR 大版本,不同Major可以不兼容,认为是代码隔离的

minor一般是做新增函数或功能的,保持在major下做到前后兼容

patch代码bug的修复

V1.3.0 V2.3.0 来自于git里tag的概念

另一个版本规则 基于commit的伪版本:版本前缀-时间戳(提交)-提交的哈希码前缀,提交commit后,go默认生成一个伪版本号。

image-20220510210106638

indirect关键字: A对B是直接依赖。没有直接导入的模块会标识为非直接依赖。

image-20220510210201412

标识出来可能会存在一些不兼容的代码逻辑。

image-20220510210334577

选B,选择最低的兼容版本,首先保证是兼容的(都是V1)

image-20220510210602016

依赖分发怎么理解:表示我们的依赖去哪里下载

github 代码托管的系统平台,我们go.mod里定义的依赖最终其实都可以对应到多版本代码仓库管理系统中的某一个项目特定版本,这样的话对于go.mod中定义的依赖,直接可以从对应仓库中下载到指定的软件依赖来完成依赖分发。

但直接使用管理仓库下载依赖的话会存在多个问题:

image-20220510210845932

为了解决这个问题,出现了Proxy:image-20220510210932029

其实是一个站点,会缓存源站中的内容,实现了稳定和可靠的依赖分发。

这个解决方案可以类比实际项目中的一些需求,比如有些场景,有些需求可能不一定能满足下游的接口,可以通过建一层Proxy的形式,或者说适配器的方案来解决问题。

image-20220510211129276

go module通过goproxy环境变量来空值Proxy配置

url列表+direct(代表如果前面这些站点都没有依赖的话会回源到第三方代码平台上去)

上图是查找依赖的路径proxy1中不存在会下放到proxy2

image-20220510211338934

默认直接go get会拉去Major版本最新的提交,也可以指定一个分支获取分支的最新commit

image-20220510211441351

必须步骤。

download把所有的依赖拉下来。 每次提交代码之前都可以执行以下go mod tidy,主要是为了代码修改后更新依赖,减少构建项目的时间。

image-20220510211608712

image-20220510211626500

3 测试

image-20220510211651947

image-20220510211707545

image-20220510211755282

测试的类型:image-20220510211806971

回归测试:通过终端回归一些主要的场景比如刷刷抖音看看评论;

集成测试:对系统功能进行验证(自动化)

单元测试:面对测试开发阶段,开发者对单独的模块进行验证。一定程度上决定了代码的质量。

3.1 单元测试

image-20220510211913419

单元包含接口、函数模块或一些聚合的大的函数

image-20220510212410951

m.Run()代表跑项目下所有单元测试

image-20220510212629323

单元测试的运行:如果用goland可以直接运行或用快捷键: VSCODE中 go test -v 测试文件.go 源文件.go

image-20220510213212178

image-20220510213244453

assert测试包

评估单元测试:image-20220510213425597

image-20220510213440666

三行中执行了前两行,所以是66.7%

image-20220510213601608

不断对各个分支进行测试,完备测试。

image-20220510213622150

单元测试的依赖:image-20220510213727537

幂等:重复运行测试时结果与之前一样

稳定:单元测试相互隔离

image-20220510213822951

打开文件,遍历每一行。如果文件被篡改 测试就被影响了。

image-20220510213959256

monkey:开源的mock测试包

打桩:用函数A去替换函数B,则B是原函数,A是打桩函数。图上以为函数打桩为例。

target表示原函数,replacement是打桩函数,Unpatch是为了保证测试结束以后把桩系在这里。

mock的实现主要是在运行时,通过go的包将内存中函数地址替换成运行时函数地址,最终测试时其实调用的是打桩函数,就是建了一个Mock的功能。

下面用刚才讲的Monkey的mock能力,再回到最初的测试:image-20220510214848863

对ReadFirstLine函数判断目标做打桩操作,让它默认一直在输出line110

通过mock实现了不对file文件的强依赖。(外部依赖

3.2 基准测试

image-20220510215519996

基准测试是指测试一段程序运行的性能和CPU的损耗。

举一个服务器负载均衡的例子:image-20220510215612559

image-20220510215645565

基准测试以Benchmark开头 入参*testing.B。首先INIT服务器的一个列表,resetTimer是定时器的重置,因为init不属于时间损耗,所以把它拋掉。

下面for循环是做串行的压力测试。

基准测试也支持并行Parallel,通过RunParallel函数实现。

红圈是每次执行的cpu耗时。发现并行其实性能是有劣化,因为Select方法用到了rand函数,rand函数为了保证全局的随机性和并发安全,持有了一把全局锁,这样在一定程度上降低了性能,多协程并行测试下会有劣化。

解决rand性能劣化问题:image-20220510220254579

fastrand牺牲了一定的随机数的一致性。

image-20220510220346580