这是我参与[第五届青训营]伴学笔记创作活动的第2天。
前言:
本次工程进阶学习主要从以下四个方面进行学习掌握,旨在通过通识课程的教学进阶到实践操作。能够了解线程工作原理,包括通道、CSP、并行安全lock、waitgroup等基本概念,对于依赖管理、依赖分发以及工具有基本认识。
- 语言进阶:从并发编程视角了解Go的高性能本质。
- 依赖管理:了解Go语言依赖管理的演进路线。
- 测试:从单元测试实践出发,提升质量意识。
- 项目实战:通过项目需求、需求拆解、逻辑代码实现感受真实的项目开发。
01.并发VS并行
并发:共行,处理多个同时性活动。 并行:使得多个程序可以在同一时刻、不同CPU上执行,可以理解成是实现并发的一种手段。 Go通过高效调度,利用计算资源,可以充分发挥多核优势,可以说Go就是为并发而生的。
1.1Goroutine
协程:用户态,轻量级线程,栈MB级别。 线程:内核态,线程跑多个协程,栈KB级别。
1.2CSP
CSP是通信顺序进程,是一种并发编译模型,是一个很强大的并发数据模型,用于描述两个独立的并发实体通过共享的通讯channel进行通信的并发模型。
1.3channel
make(chan 元素类型,[缓冲大小]) 无缓冲通道 make(chan int) 有缓冲通道 make(chan int,2) 通道遵循先入先出的规则,保证数据的收发顺序。
- 本身是一个队列,先进先出
- 线程安全,无需加锁
- 本身具有类型,如果需要保存多个类型。则定义成interface类型
- 是引用类型,需要make,一旦make,则容量确定,不会动态增减
1.4并发安全lock
协程的执行顺序不确定导致;CPU的内存模型(多级缓存和额内存)导致。实现线程安全可以使用以下方法:(1)使用通道实现线程安全;(2)使用锁实现线程安全。
1.5WaitGroup
是一个计数信号量,可以用来记录并维护运行的线程。waitgroup>0,则wait方法阻塞。
02.背景
工程项目不可能基于标准库0~1编码搭建;管理依赖库。 三要素:
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
2.1Go的依赖管理演进
GOPATH->Go Vendor->Go Module 不同环境(项目)依赖的版本不同,需要控制依赖库的版本。
2.1.1GOPATH
- 环境变量$GOPATH
bin:项目编译的二进制文件 pkg:项目编译的中间产物,加速编译 src:项目源码
- 项目代码直接依赖src下的代码
- go get下载最新版本的包到src目录下
弊端:无法实现package的多版本控制。
2.1.2Go Vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/wendor
- 依赖寻址方式vendor=>GOPATH
通过每个项目引入一份依赖的副本,解决多个项目需要同一个package依赖的冲突问题。
弊端:不能很好控制版本选择问题,一旦更新项目,可能带来依赖冲突导致编译错误。
2.1.3Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
最终目标:定义版本规则和管理项目依赖关系
2.2依赖管理三要素
1.配置文件,描述依赖 go.mod 2.中心仓库管理依赖库 Proxy 3.本地工具 go get/mod
2.3.1依赖配置-go.mod
module example/project/app 依赖管理基本单元:模块路径(标识模块) go 1.16 原生库 require(...) 描述单元依赖,每部分由两部分组成:仓库 版本号
2.3.2依赖配置-version
- 语义化版本
�����.MAJOR.{MINOR}.${PATCH}
- 基于commit伪版本
vX.0.0-yyyymmddhhmmss(时间戳)-abcdefgh1234(哈希校验码前缀)
2.3.3依赖配置-indirecct
表示go.mod对应的当前module没有直接导入到包,即非直接依赖关系。
2.3.4依赖配置-incompatible
主版本2+模块会在模块路径增加/vN后缀 对于没有go.mod文件并且主版本2+的依赖,会+incompatible进行标识
2.3.4依赖配置-依赖图
Go选择最低的兼容版本,即最终编译时,下图所用的项目c的版本为v1.4。
2.3.5依赖分发-回源
弊端:
- 无法保证构建稳定性(增加/修改/删除软件版本)
- 无法保证依赖可用性(删除软件)
- 增加第三方压力(代码托管平台负载问题)
2.3.5依赖分发-Proxy
Go Proxy 就是解决上述问题的方案。Go Proxy 是一个服务站点,它会缓存 GitHub 中的代码内容,缓存的代码版本不会改变,并且在 GitHub 作者删除了代码之后也依然可用,从而实现了 “immutability” (不变性) 和 “available” (可用的) 的依赖分发。使用 Go Proxy 后,构建时会直接从 Go Proxy 站点拉取依赖。
2.3.6依赖分发-变量GOPROXY
GOPROXY = "proxy1.cn, proxy2.cn, direct" 服务站点URL列表,“dirct”表示源站。
2.3.7工具-go get
2.3.8工具-go mod
03测试(单元测试、Mock测试、基准测试)##
- 回归测试:我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,正确的做法应该是完整运行所有的测试用例,保证不会因为修改代码而引入新的问题。
- 集成测试:系统功能测试验证,集成一个功能问题。
- 单元测试:单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次有保证能够代码覆盖率达到 100% 的测试,是整个软件测试过程的基础和前提,单元测试防止了开发的后期因 bug 过多而失控,单元测试的性价比是最好的。
- 从上到下,覆盖率逐层变大,成本逐层降低。
3.1单元测试
对软件中的最小可测试单元进行检查和验证。
3.1.1单元测试-规则
- 所有测试文件以_test.go结尾;
- 初始化逻辑放到TestMain中
func TestMain(m *testing.M) { 测试前:数据装载,配置初始化等前置工作 code :=m.Run 测试后:释放资源等收尾工作 os.Exit(code) }
- func TestXxx(*testing.T)
func TestPubilcPost(t *testing.T){
3.1.2单元测试-例子
本来应该输出tom,最后因为代码逻辑问题输出jerry。
对输出和期望输出进行对比,如果不同,则表示整个代码有误。
3.1.3单元测试-assert
3.1.4单元测试-覆盖率
单元测试覆盖率是衡量代码质量的一个重要指标,重要的代码文件覆盖率应该至少达到80%以上。直接执行测试代码的指令: go test 指定文件名称 -run “测试方法”。
3.1.5单元测试-Tips
- 一般覆盖率:50%~60%,较高覆盖率80%以上。
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
3.2单元测试-依赖
外部依赖=>稳定&幂等
3.3单元测试-文件处理
测试单个文件,一定要带上被测试的源文件 go test -v cal_test.go cal.go
3.4单元测试-Moke
monkey:github.com/bouk/monkey
- 快速Moke函数
- 为一个函数打桩
- 为一个方法打桩
打桩就是创建mock 桩,指定 API 请求内容及其映射的 mock 响应内容;所谓调桩就是被测服务来请求 mock 桩并接收 mock 响应。mock有两种。一种是静态打桩,一种是动态打桩。静态打桩就是在写测试代码之前根据需要打桩的类生成另外一个类,这个类就是mock object。动态打桩就是mock object是在测试代码运行的时候才生成的。比较常用的mock工具有EasyMock、Jmock、PowerMock、MockMvc。
对ReadFirstLine打桩测试,不再依赖本地文件。
3.5基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
- 使用方法类似单元测试
基准测试可以测试一段程序的运行性能及耗费 CPU 的程度。
3.5.1基准测试-例子
随机选择执行服务器
3.5.2基准测试-运行###
3.5.3基准测试-优化
Fastrand
04项目实践
需求设计-代码开发-测试运行
4.1需求描述
社区话题页面
- 展示话题(标题、文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
4.2需求用例
4.3ER图
topic和post是实体,一对多关系。
4.4分层结构
- 数据层:数据model,外部数据的增删改查
- 逻辑层:业务entity,处理核心业务逻辑输出
- 视图层:视图view,处理和外部的交互逻辑
4.5组件工具
- Gin高性能go web框架
- Go Mod
go mod init go get gopkg.in/gin-gonic/gin.v1@v1.3.0
4.6Repository
4.6.1Repository-index
数据行映射成内存的map来实现内存的索引,因为map的时间复杂度为O(1),所以能够很快进行查询。
4.6.2Repository-查询
索引:话题ID 数据:话题
索引:话题ID 数据:帖子列表
4.7Service
定义结构体函数,内部设置两个实体。流程是先进行参数校验,进行非0等非法校验;然后通过ropository准备数据,最后组装实体。
-
checkparam()对ID进行校验,保护服务端的service。
-
prepareinfo()的实现,主要是获取repository层话题数据和回帖数据,两个流程没有前后相互依赖,可以考虑用并行来提高效率。
- 主要是通过waitgroup
- wg.add(2):有两个并行任务。
- go func(){...}调用方法进行数据查询。
- wg.wait()进行阻塞,等待话题信息、回帖信息从repository层返回。
4.8Controller
构建view对象,业务错误码。
4.9Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
4.10运行
运行测试go run server.go
总结
上半节课主要是介绍了Go的通信原理,以及线程、管道、CSP、依赖管理等知识,了解Go的架构以及高性能。下半节课主要学习了Go在测试环节所进行的单元测试、Moke测试和基准测试,对于项目的设计我们可以通过三个步骤进行:项目拆解,代码设计,测试运行。整体下来进度较快,稍不认真就会跟不上进度,对于Go的学习不能只局限于录播课程学习,更应该课下查阅资料与代码操练。