前言
这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记,做笔记记录一下自己的学习过程。
此笔记主要内容如下:
- 语言进阶
- 依赖管理
- 测试
1 语言进阶
1.1 Goroutine
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个协程,栈KB级别
在Go中,函数前加go关键字可以为函数拉起一个协程运行,但要注意的是主协程在运行结束时会回收子协程。
1.2 CSP(Communicating Sequential Process)
在Go中,提倡通过通信共享内存而不是通过共享内存而实现通信。
1.3 Channel
在Go中,用make创建Channel。
make(chan 元素类型,[缓冲大小])
- 无缓冲通道(同步通道) make(chan int)
- 有缓冲通道(生产消费模型) make(chan int,2)
如下图,A和B子协程之间通过src和dest通道实现通信,并且B设置缓冲通道保证消费比生产慢。
1.4 并发安全 Lock
对变量执行2000次+1操作,5个协程并发执行。
如果不加锁有可能两个协程之间读到同一个x值。
1.5 WaitGroup
从WaitGroup的文档中可以看到,WaitGroup暴露了3个方法,Add()、Done()、Wait()。相当于维护了一个计数器,开启协程时+1(Add),执行结束-1(Done),主协程阻塞直到计数器为0(Wait)。
2 依赖管理
2.1 Go依赖管理演进
graph LR
A[GOPATH] --> B[Go Vendor]
B --> C[Go Module]
迭代目的:
- 不同环境(项目)依赖版本不同
- 控制依赖库的版本
2.1.1 GOPATH
- 环境变量 $GOPATH
- 项目代码直接依赖src下的代码
- go get 下载最新版本的包到src目录下
弊端:
- 工程A和工程B依赖于某一个package的不同版本,无法实现package的多版本控制
2.1.2 Go Vendor
- 项目目录下新增vendor文件,所有依赖包副本形式放在$ProjectRoot/vendorr
- 依赖寻址方式: vendor => GOPATH 通过每个项目引入一份依赖的副本,解决了多个项目依赖同一个package的冲突问题。
弊端:
- 无法控制依赖的版本
- 更新项目又可能出现以来冲突,导致编译出错。
2.1.3 Go Module
Go 1.11 默认引入,Go 1.16默认开启。
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
2.2 依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
2.3 依赖配置
2.3.1 go.mod
依赖标识:[Module Path][Version/Pseudo-version]
即模块路径+版本 唯一标识
2.3.2 version
语义化版本
${MAJOR}.${MINOR}.${PATCH}
比如: v1.0.2
基于commit伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
对于语义化版本,MAJOR属于大版本,不同MAJOR可以不兼容,可以认为是代码隔离。MINOR通常是做一些新增函数或功能,需要保持在同一个MAJOR下做到前后兼容。PATCH一般做一些代码BUG的修复。
对于commit版本,首先由语义化版本前缀作为第一部分,第二部分是提交某次commit的一个时间戳,第三部分是提交某次commit的12位哈希校验码的前缀。
2.3.3 indirect
对于A->B-C这种依赖,A对B是直接依赖,A对C是间接依赖。在go.mod中,没有直接导入该依赖模块的包,标记为indirect。
2.3.4 incompatible
即可能存在不兼容的情况。
- 主版本2+模块会在模块路径增加/vN后缀
- 对于没有go.mod文件并且主版本2+的依赖,会在版本号后加上+incompatible后缀
- Go在编译时会选择最低的兼容版本进行编译
"图片源自青训营ppt"
2.3.5 依赖分发-回源&Proxy
依赖分发也就是从哪里下载依赖包的问题。但是Go Modules系统中定义的依赖最多对应到多版本代码管理系统中某一项目的特定提交或版本,由于软件作者可以直接增加、修改或删除软件版本,就带来以下问题:
- 无法保证构建稳定性 - 增加/修改/删除软件版本
- 无法保证依赖可用性 - 删除软件
- 增加第三方压力 - 代码托管平台负载问题
而Go Proxy可以解决问题。Go Proxy是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且源站删除软件之后仍然可用。使用Go Proxy构建时会直接从Go Proxy站点拉取依赖。
GOPROXY="https://proxy1.cn,https://proxy2.cn,direct"
GOPROXY是服务器站点URL列表和“direct”源站的字符串。
2.3.6 工具 - go get/mod
3 测试
测试一般分为回归测试、集成测试和单元测试。 回归测试一般是QA(质量保证)同学通过终端手动回归一些固定的主流程场景测试。 集成测试是对系统功能维度做测试验证,通过服务暴露的某些接口进行自动化的测试。 单元测试面对测试开发阶段,开发者对单独的函数、模块做功能验证。
层级由上至下,测试成本逐渐减低,而测试覆盖率却逐步上升。所以说单元测试的覆盖率一定程度上决定着代码的质量。
3.1 单元测试
单元测试主要包括:输入,测试单元,输出,以及校对。
单元的概念比较广,包括接口,函数模块等;用最后的校对来保证代码的功能与我们预期相符;
单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有BUG的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。
3.1.1 单元测试-规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
3.1.2 单元测试-assert
很多assert包可以帮助我们做单元测试。
3.1.3 单元测试-覆盖率
用go test xxx_test.go xxx.go --cover可以查看单侧的覆盖率。
3.1.4 单元测试-Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+(资金型服务)
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
3.1.5 单元测试-依赖
工程中复杂的项目,一般会依赖DB、Cache和本地File(容易被改动,被改动了我们单元测试可能就进行不了),而我们单元测试需要保证稳定性和幂等性。稳定性是指单元测试能够相互隔离,能在任何时间任何环境下运行测试。幂等性是指每一次测试运行都应该产生与之前一样的结果。而要实现这一目的就要用到Mock机制。
3.1.6 单元测试-Mock
这里我们用monkey做Mock测试。monkey是一个开源的mock测试包,可以对method或者实例的方法进行mock,即打桩Patch。
Monkey Patch作用在运行时通过Go的unsafe包,将内存中函数的地址替换为运行时函数的地址,最终测试时调用的是打桩函数。
3.2 基准测试
Go语言还提供了基准测试框架,基准测试是指测试一段程序的运行时的性能和CPU的损耗程度。在实际项目开发中,为了定位问题经常要对代码做性能分析,这就用到了基准测试。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
基准测试规范与单元测试类似,前缀换成了Benchmark。
运行结果: