这是我参与「第五届青训营 」笔记创作活动的第2天
一、本堂课重点内容
- Go语言进阶
- Go依赖管理
- Go测试
- Go项目实战
二、详细知识点介绍
Go语言进阶
并发 VS 并行
并发:多个程序在一个核上跑
并行:多个程序在多个核上跑
Go语言可以充分发挥多核优势,高效运行
Goroutine
goroutine(也称为协程)是go语言所提出的一种并发机制。相比于线程(一般在内核中实现),协程(一般在用户空间实现)的开销更小,栈空间一般为KB级别,而线程一般为MB级别,一个线程对应多个协程。因此协程也被称为轻量级线程。
CSP并发模型
go语言提倡以通信来实现共享内存,而不是以共享内存实现通信。传统的并发模型通常以共享内存的形式加上锁的机制来实现通信。go语言在传统并发模型基础上,提出了以goroutine+channel的CSP并发模型。
Channel
If goroutines are the activities of a concurrent Go program, channels are the connections between them.
以上这句话引用自 《The Go Programming Language》 ,即所谓的“GO语言圣经”。可以说这句话很形象的解释了Channel是作为连接两个goroutine的媒介。
可以使用以下方式创建Channel:
make(chan int) //无缓冲通道
make(chan int, 2) //有缓冲通道
其中有缓冲通道可以很好的解决读写速度不匹配的问题。
并发安全Lock
一般不使用,效率较低。
WaitGroup
常用于实现多个goroutine之间的同步。其中包含三个方法:
Add(delta int) //计数器
Done() //计数器-1
Wait() //阻塞直到计数器为0
在实际使用时,常在主线程使用wg.Add(N)将计数器加到N,然后在每个goroutine结束时中使用defer wg.Done()来将计数器减一,而在主线程中在使用wg.Wait()来等待各goroutine结束。
Go依赖管理
背景
实际的工程项目,不可能从0到1搭建所有组件,我们更多是关注业务层面的实现,而其他的依赖一般都会通过SDK的方式来引入,因此对于依赖的管理是非常重要的。
Go依赖管理的演进
Go的依赖管理主要经历了三个阶段分别是GOPATH、Go Vender、Go Module,整个演进路线主要围绕实现两个目标来迭代发展,分别是:
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
GOPATH
GOPATH是go语言支持的一个环境变量,其值是Go项目的工作区,工作区下有三个目录:
- bin存放Go项目编译生成的二进制文件
- pkg存放编译的中间产物,加快编译速度
- src存放Go项目的源码,
go get下载的包将存放至此
GOPATH的弊端:src下只能存在一个版本的包。因此如果有两个依赖不同版本的包的项目,那么就无法保证编译通过。
Go Vender
为了解决GOPATH的问题,出现了Go Vender,Vender是位于项目目录下的一个文件夹(项目位于src目录下),该文件夹中存放的是当前项目的依赖副本。在该机制下,如果存在vender文件夹,将会优先使用该文件夹下的依赖,若不存在则从GOPATH中寻找。
Go Vender的弊端:vender不能很清晰的标识依赖的版本概念。假如有一个项目依赖于包B和包C,而包 B和包C都依赖于同一个包D,但是包B和包C所依赖的包D版本不同,这样就无法控制依赖的版本(vender中同样也只能存在一个唯一版本的包),此外若更新最新版,则又有可能导致编译错误。
Go Module
Go Module是Go官方推出的依赖管理系统,解决了之前的依赖管理系统的诸多问题。从Go1.11开始Go Module就被实验性的引入,到了1.16默认开启,也被叫做go mod。
使用go mod实现依赖管理有三要素
go.mod: 配置文件,描述依赖
配置文件由三部分组成,分别是模块路径(模块存在的标识)、原生库(标识go版本号)、单元依赖(描述该模块的依赖,其由模块路径+版本两部分唯一标识)。
对于之前两种方式来说,都没有版本规则的概念,而go mod则为了方便管理而定义了版本规则,分为语义化版本和基于commit伪版本,其中语义化版本包含三部分,${MAJOR}.${MINOR}.${PATCH},major标识主版本号,不同的主版本号之间一般不兼容,minor标识次要版本号,主要用于新增功能,patch一般用于标识补丁。而基于commit伪版本其基本样式为vX.0.0-yyyymmddhhmmss-abcdefgh1234,前面的基础版本号和语义化版本相同,后面是时间戳(提交时间),最后是校验码信息(12位哈希前缀)。
在单元依赖中,依赖行最后以indirect标识的依赖是非直接依赖,也就是没有直接导入该依赖的包。在依赖的模块路径中,主版本号大于2的通常会在模块路径后添加/vN后缀,N表示大于2的主版本号,对于一些在提出go mod之前主版本号就已经大于2的,为了兼容,会在版本号后加上incompatible
为了解决之前的Go Vender问题,对于同一大版本下的两个不同版本的包,可以选择最低的兼容版本,这样保证两个包都能正常运行。而对于不同大版本下的包,则在远程仓库内部建立了多个目录,多个不同的模块之间使用不同的go.mod文件,文件中引用不同版本的依赖即可。
Proxy: 中心仓库管理依赖库
解决了依赖的配置问题,现在解决依赖从哪获取的问题,github是常见的代码托管平台,不过,其不适合作为管理依赖的平台,因为软件作者可以随时在代码平台增删改软件的版本,从而导致下次使用时无法保证依赖可用。而go proxy就是用来解决该问题的,go proxy是一个服务站点,其缓存了源站(代码原始网站)中的内容,版本不会改变,因此源站删除后依旧可用。
go proxy通过GOPROXY环境变量来控制,该环境变量是一个URL列表,往往在列表最后会加上direct,用来表示源站,寻找依赖时,往往会从前往后依次寻找。
go get/mod: 本地工具
go get工具用于从远程获取并安装好依赖,一般的使用方法如下:
go get example.org/pkg
还可以添加一些后缀:
@update默认@none删除依赖@v1.1.2指定语义版本@23dfdd5特定的commit@master分支的最新commit
go mod 工具用于包管理,一般使用方法如下:
go mod init //初始化,创建go.mod文件
go mod download //下载模块到本地缓存
go mod tidy //增加需要的依赖,删除不需要的依赖
提前之前,执行go mod tidy可以有效减少无效依赖包的拉取。
Go测试
测试是实际工程开发中的一项重要内容,主要包括单测规范、测试mock以及基准测试。
测试一般分为回归测试、集成测试和单元测试。从前往后测试的成本逐渐降低,而测试的覆盖率(测试中所运行到的代码占总体代码的比例)却逐步上升。其中回归测试一般是质量保证(QA)通过终端回归一些固定的使用场景,集成测试主要是对系统的功能维度做测试验证,单元测试则是测试开发阶段,对单独函数、模块进行功能性验证。因此单元测试一定程度上决定了代码的质量。
单元测试
单元测试主要包括输入、测试单元、输出以及校对,单元的概念包括函数、接口、模块等,最后的校对用来保证功能与预期相符合。
单元测试的规则
- 所有测试文件以
_test.go结尾 - 测试函数声明为
func TestXxx(t *testinf.T)注意Test后的第一个单词要大写 - 初始化逻辑放入
func TestMain(m *testing.M)中 - 使用
go test [flags] [packages]来运行测试
单元测试-Mock
在项目中往往会有很多复杂的依赖,而单元测试需要保证稳定性和幂等性。稳定性是指相互隔离,无论什么时候,什么环境,运行测试结果都应该相同。而幂等性指的是每一次测试都应该与之前结果一样。要实现上述两种目的就要使用mock机制。
mock指的就是对函数或实例进行模仿,反射,指针赋值。monkey就是一个开源的mock测试库,monkey有一个Patch函数可以对函数打桩,其在Runtime时,通过unsafe包将内存中函数的地址替换为运行时函数的地址。
基准测试
基准测试是指测试一端程序的运行性能以及耗费CPU的程度。使用方法与单元测试类似,此处不再多介绍。
三、实践练习例子
这次的课程讲解了掘金社区话题页面的服务端接口实现过程。主要包括:
- 展示话题(标题,文字描述)和回帖列表
- 暂时不考虑前端页面的实现,仅仅实现一个本地web服务
- 话题和回帖数据使用文件存储
需求用例
用户浏览页面,页面包括两大部分,一部分是Topic,另一部分是PostList。
E-R关系
此处定义了两个实体模型
erDiagram
Topic ||--o{ Post : I
Topic {
int64 id
string title
string content
int64 create_time
}
Post {
int64 id
int64 topic_id
string content
int64 create_time
}
分层结构
整体分为三层,数据层、逻辑层和视图层。
数据层关联底层数据模型,封装外部数据的增删改查,数据层面向逻辑层,对其透明,屏蔽下游数据差异。
逻辑层处理核心业务逻辑,计算打包业务实体,对于我们的需求就是Topic和PostList,并上送给视图层。
视图层负责处理和外部交互的逻辑,对于我们的需求就是封装JSON格式的请求结果,以api的形式访问。
组件工具
- 高性能Gin框架
- Go Mod
四、课后个人总结
通过此次课程,我了解到许多工程开发技巧,既有高性能的并发编程,也有保证代码质量的单元测试,还有重要的依赖管理,最后还通过一个实战例子将所学的知识运用了起来,对我来说,这次课程收获很大。