前言
这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
今日学习内容:
- 语言进阶
- 依赖管理
- 测试
- 项目实战
正文
语言进阶
从并发编程的视角了解 Go 高性能的本质
- 并发 VS 并行
- 并发:多线程程序在一个核的 CPU 上运行(时间片切换)
- 并行:多线程程序在多个核的 CPU 上运行
Go 可以充分发挥多核优势,高效运行
- 线程 VS 协程
- 线程:内核态,线程可以运行多个协程,栈为 MB 级别
- 协程:用户态,轻量级线程,栈为 KB 级别
-
CSP(Communicating Sequential Process)
GO 提倡通过通信来共享内存而不是通过共享内存来实现通信
-
Channel
Channel 是 Go 中的一个核心数据类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。它的操作符是箭头 <-
。
make(chan 元素类型,[缓冲大小])
- 无缓冲通道(同步通道): make(chan int)
- 有缓冲通道: make(chan int, 2)
- 互斥锁(sync.Mutex)VS 读写互斥锁(sync.RWMutex)
Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex
和 sync.RWMutex
。
- Mutex 是最简单的一种锁类型,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。
- RWMutex 相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个 goroutine 可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占。
从 RWMutex 的实现看,RWMutex 类型其实组合了 Mutex
type RWMutex struct {
w Mutex
writerSem uint32
readerSem uint32
readerCount int32
readerWait int32
}
对于这两种锁类型,任何一个 Lock() 或 RLock() 均需要保证对应有 Unlock() 或 RUnlock() 调用与之对应,否则可能导致等待该锁的所有 goroutine 处于饥饿状态,甚至可能导致死锁。
- WaitGroup
WaitGroup
对象内部有一个计数器,最初从 0 开始,它有三个方法:Add(), Done(), Wait()
用来控制计数器的数量。Add(n)
把计数器设置为n
,Done()
每次把计数器-1
,wait()
会阻塞代码的运行,直到计数器的值减为0。
需要注意的是:
- WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址
- 不能使用
Add()
给计数器设置成一个负值
依赖管理
了解 Go 原因依赖管理的演进路线
- 为什么需要对依赖进行管理
- 工程项目不可能基于标准库从 0 - 1 编码搭建,势必会依赖其他第三方库
- 不同环境(项目)依赖的版本各不相同,因此需要使用依赖管理来控制各个依赖库的版本
- Go 的依赖管理演进历史
- Go 的依赖管理主要经历了三个阶段,从
GOPATH -> Go Vendor -> Go Module
(当前主流)
下面分别介绍 GOPATH、GO Vendor、GO Module
GOPATH
GOPATH 是 GO 语言支持的一个环境变量,其 value 是 Go 项目的工作区。
GOPATH 的弊端
在 GOPATH 的管理模式下,如果多个项目依赖同一个库,则该依赖库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这显然是无法满足我们的项目依赖需求。为了解决这个弊端,于是又出现了 Go Vendor。
GO Vendor
Vendor
是当前项目下的一个目录,其中存放了当前项目所有依赖副本。在 Vendor 机制下,如果当前项目存在 Vendor 目录,会优先使用该目录下的依赖,如果依赖不存在,则会从 GOPATH 中寻找。这样可以解决多个项目需要同一个 Package 依赖(不同版本)的冲突问题。
GO Vendor 的弊端
Vendor 无法很好地解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题。归根到底是 Vendor 不能很清晰的标识依赖的版本概念。因此,Go Module 就应运而生了。
GO Module
Go Module 是 Go 语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题。它通过
go.mod
文件管理依赖包版本,通过go get/go mod
指令工具管理依赖包。
依赖管理三要素
- 配置文件,描述依赖(go.mod)
- 中心仓库管理依赖库(Proxy)
- 本地工具(go get/mod)
测试
单元测试、Mock 测试、基准测试
- 单元测试
测试一般分为回归测试、集成测试、单元测试。回归测试一般是 QA 同学手动通过终端回归一些固定的主流场景,集成测试是对系统功能维度做测试验证,而单元测试是在开发阶段,开发者对单独的函数,模块做功能验证,层级从上到下,测试成本逐渐降低,而测试覆盖率逐步上升,所以单元测试的覆盖率一定程度上决定了代码的质量。
单元测试的规则
单元测试-代码覆盖率
代码覆盖率用于衡量代码是否经过了足够的测试,评价项目的测试水平。如下图,通过go test xx_test.go xx.go --cover
命令可以计算单元测试的代码覆盖率。
- Mock 测试
单元测试-外部依赖
对于复杂的工程项目,一般会有数据库,缓存等外部依赖,而单元测试需要保证稳定性和幂等性。其中稳定性是指相互隔离,能在任何时间,任何环境,运行单元测试。幂等性是指每一次测试运行都应该产生与之前一样的结果。为了实现这一个目的,就要用到 Mock 机制。
单元测试-文件处理
如下图的单元测试,需要依赖本地的文件,如果文件被修改或者删除,单元测试就会 fail,为了保证测试 case 的稳定性,我们对读取函数进行 mock,屏蔽对于文件的依赖。
单元测试-Mock
这里使用了一个开源的 mock 测试框架—Monkey(github.com/bouk/monkey… ,它支持对函数或者实例的方法进行 mock。Monkey Patch 的作用域在 runtime,在运行时通过 Go 的 unsafe 包,能够将内存中函数的地址替换为运行时函数的地址。
下图是一个 mock 的使用样例,通过 patch 对 ReadFirstLine 进行打桩 mock,默认返回 line110,然后通过 defer 卸载 mock,这样整个测试函数就摆脱了本地文件的束缚和依赖。
- 基准测试
基准测试指测试一段程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析。这就用到了基准测试,使用方法类似于单元测试。
项目实战
通过项目需求、需求拆解、逻辑设计、代码实现感受真实的项目开发
待续~