Go语言进阶及项目实战 | 青训营笔记

178 阅读9分钟

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

一、 本堂课重点内容:

1.      语言进阶

2.      依赖管理

3.      测试

4.      项目实战

二、详细知识点介绍:

2.1 Go语言并发编程:

       Go语言从语言层面支持并发,可以充分发挥多核优势,高效运行。

       基本概念:

       线程:存在于用户态,是轻量级的进程,栈开销在MB级别。

       Go协程:一个线程可以跑多个协程,栈开销在KB级别。

       简单来说:Go协程是用户层面对操作系统线程的一层复用。

2.2 Go协程与线程的区别:

       2.2.1 内存占用: 创建一个 goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存,而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离。

       对于一个用 Go 构建的 HTTP Server 而言,对到来的每个请求,创建一个 goroutine 用来处理是非常轻松的一件事。而如果用一个使用线程作为并发原语的语言构建的服务,例如 Java 来说,每个请求对应一个线程则太浪费资源了,很快就会出 OOM 错误(OutOfMermoryError)。

       2.2.2 创建和销毀

       Thread 创建和销毀都会有巨大的消耗,因为要和操作系统打交道,是内核级的,通常解决的办法就是线程池。而 goroutine 因为是由 Go runtime 负责管理的,创建和销毁的消耗非常小,是用户级。

       2.2.3 切换

       当 threads 切换时,需要保存各种寄存器,以便将来恢复:

【16 general purpose registers, PC (Program Counter), SP (Stack Pointer), segment registers, 16 XMM registers, FP coprocessor state, 16 AVX registers, all MSRs etc.】

  而 goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。

一般而言,线程切换会消耗 1000-1500 纳秒,一个纳秒平均可以执行 12-18 条指令。所以由于线程切换,执行指令的条数会减少 12000-18000。Goroutine 的切换约为 200 ns,相当于 2400-3600 条指令。

因此,goroutines 切换成本比 threads 要小得多。

2.3 通道

       语法:make(chan 元素类型,[缓冲大小])

       如果指定了缓冲大小,则为有缓冲通道,如果没有指定,则为无缓冲通道。

       简单生产者-消费者模型:

image.png

并发安全:当我们利用5个协程对同一变量执行2000次 +1 操作,如果未加锁,因为并发过程中的不确定性则结果不固定,面对这种情况,我们可以利用sync包中的lock函数进行加锁,保证并发正确执行。

image.png

同时我们也可以通过waitGroup函数,sleep函数控制并发流程。

2.4 依赖管理:

       对于简单的单体函数只需要原生的SDK,而实际工程比较负载,不可能基于0~1编码搭建,更多的关注业务逻辑的实现,而其他涉及框架,日志,diver,以及collection等一系列依赖都会通过SDK引入,这样对依赖包的管理就尤为重要

       2.4.1Go语言依赖管理演进:

       Go语言依赖管理三个阶段:GoPATH->Go Vendor->Go Module

       GoPATH:GoPATH是Go语言支持的一个环境变量,value是Go项目的工作区,目录结构如下:src:存放Go项目的源码,pkg:存放编译的中间产物,bin:存放Go语言编译生成的二进制文件。

image.png

       弊端:无法实现package的多版本控制。

       Go Vendor:Go Vendor在项目目录下增加了vendor文件,所有依赖包以副本形式存放在$ProjectRoot/vendor下。依赖寻址方式:vendor->GoPATH。它通过每个项目引入一个依赖的副本解决了多个项目需要同一个package依赖的冲突问题。

       弊端:无法控制依赖的版本。

                  更新项目可能出现依赖冲突,导致编译出错。

       Go Module:通过go,mod文件管理依赖包版本,通过go get/go mod指令工具管理依赖包。

       2.4.2依赖管理三要素:

       1.配置文件,描述依赖                     go,mod

       2.中心仓库管理依赖库                       Proxy

       3.本地工具                                    go get/mod

       go.mod:

image.png 首先模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是gitub前缀则表示可以从github仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的 init go,mod文件进行管理。下面是依赖的原生sdk版本。

最下面是单元依赖,每个依赖单元用模块路径+版本来唯—标示。

依赖单元中的特殊标识符:首先是indirect后缀,表示go,mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖。标示为间接依赖。

下一个常见的是incompatible,主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块处理同一个项目不同主版本依赖。因为在这项规则提出之前已经又一些仓库打上2或更高版本的tag了,为了兼容这部分仓库,对于有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+imcompatible后缀。

       一个结论:在最终编译时会选择兼容子项目的最低兼容版本

2.4.3 依赖分发:Proxy

image.png

Go Proxy是一个服务站点,它会缓存站中的软件内容,缓存的软件版本不会变,并且在源站软件删除后依然可用,从而实现了稳定可靠的依赖分发。

Go Proxy使用:GOPROXY=proxy1.cn,https://proxy2.cn,…

GOPROXY是一个Go Proxy站点URL列表,可以使用“direct”代表源站,对于依赖配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,则会在proxy2寻找,如果proxy2不存在则回源到源站直接下载依赖,缓存到proxy站点中。

2.5 测试

测试分类:

image.png

2.5.1:单元测试:

       单元测试主要包括输入,测试单元,输出,以及校对,单元的概念包括接口,函数,模块等;用最后的校对保证代码的功能与我们的预期相符,单元测试一方面可以保障质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又为破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。

       规则:测试文件以_test.go结尾,func TestXxx(*testing.T),初始化逻辑放到TestMain中

       Tips:一般覆盖率:50-60%,较高覆盖率80%+。

                  测试分支相互独立,全面覆盖。

                  测试单元粒度足够小,函数但以职责。

2.5.2:基准测试:

       基准测试是指测试一段程序的运行性能及耗费CPU的程度,在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题,经常要做代码性能分析,这就用到了基准测试,使用方式类似于单元测试。

image.png

       基准测试以Benchmark:开头,入参是testing.B的N值反复递增循坏测试

(对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到1秒,那么testing.B中的N值将按1、2、5、10、20、50…递增,并以递增后的值重新进行用例函数测试。)

Rettimer重置计数器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围,runparallel时多协程并发测试,执行2个基准测试,发现代码在并发情况下存在劣化,主要原因时rand为了保证全局的随机性和并发安全,持有了一把全局锁。

三、实践练习例子:

       项目实践:

3.1:需求描述: 展示话题(标题,文字描述)和回帖列表

                                   暂不考虑前端页面实现,仅仅实现一个本地web服务

                                   话题和回帖数据用文件存储

3.2  E-R图建立实体和属性之间的关系

image.png

3.3 分层结构:

image.png 整体分为三层,repository数据层,servicei逻辑层,controoler视图层

数据层关联底层数据模型,也就是model,封装外部的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,贴子数据,数据层面向逻辑层,对service透明,屏蔽下游数据差异。

Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;

Controllert视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好

3.4 组件工具:

       Gin高性能go web框架

       https://github,com/gin-gonic/gin#installation

       Go mod :go mod init , go get

3.5 仓库:

       通过索引优化话题数据查找:

image.png 首先打开文件,基于file初始化scaner,通过迭代器方式遍历数据行,转化为结构体存储至内存map,这就是初始化话题内存索引。

image.png 存在内存索引之后,可以通过查询key得到map中的值就可以了。这里使用了sync,once主要适用于高并发的场景下只执行一次的场景,这样基于once的实现模式就是我们平常说的单例模式,减少存储浪费。

3.6 Service:

image.png

3.7 Controller

image.png 构建view对象,通过code msg打包业务状态信息,用data承载业务实体新信息。

3.8 Router

image.png

四、课后个人总结:

       本节课讲了go语言的并发机制,并发的实现语法。以及项目测试的有关知识。最后的项目设计融合了前面所学的内容。不过关于并发机制仍有深入了解的需要。

五、引用参考:

关于线程-协程间的区别:goroutine · GitBook (wzcu.com)