这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go语言进阶、依赖管理与测试
语言进阶特性
GoRoutine与并发模型
Goroutine
运行在用户态的轻量级协程,与内核态的线程作M:N的映射。
使用go关键字就可以发射一个goroutine
CSP模型
即Communicating Sequential Process,提倡通过通信共享内存而不是通过共享内存实现通信。
前者是使用chan来实现同步,而后者是使用临界区来实现同步。
使用Channel
make(chan Type [,buffer size] )
从channel中读取数据与写入数据:
ch <- 1
v := <- ch
使用内建的close函数来关闭一个channel,也可以用defer将关闭操作放到函数返回时进行。
使用Lock
var(
x
lock sync.Mutex
)
sync包提供Mutex和RWMutex两种类型的互斥锁。
一个简单的例子是在不实用任何同步操作的情况下并发地对一个变量进行+1操作。使用mutex可以避免其中的竞态条件。
使用WaitGroup
sync.WaitGroup对外提供了三个方法:
- Add(delta int) 将计数器增加delta
- Done() 将计数器减一
- Wait() 等待计数器降低到0
依赖管理
Go依赖管理的演进过程
Go语言从诞生至今,其依赖管理的演进可以概括为:GOPATH => Go Vendor => Go Module
GOPATH
GOPATH是一个环境变量,表示一个路径,路径下一般包括三个部分
-bin
-pkg
-src
所有依赖的源代码保存到src目录下,通过go get下载最新版本的源码到src目录下。
缺点:无法进行多版本控制
Go Vendor
Go Vendor所使用的方式是,在当前的go项目路径下增加一个vendor文件夹,所有依赖库的副本都保存在$ProjectRoot/vendor目录下。
通过每个项目都引入一份依赖的副本,解决了多个项目依赖同一个package的不同版本问题。
缺点:假如项目A依赖package B和C,而B和C又分别依赖于不同版本的package D_V1和D_V2,vendor无法解决这种情况下的依赖冲突。
Go Module
- 通过go.mod文件来管理依赖包版本
- 通过
go get/go mod指令工具来管理依赖包
相比于前两者,Go Module管理依赖是基于版本规则的,而PATH和vendor都是直接依赖于源文件的。
依赖管理三要素
-
配置文件,用于描述依赖
go.mod文件
-
中心仓库管理依赖库
go mod中的Proxy
-
本地工具
主要是
go get和go mod两个工具
依赖配置
使用go.mod进行依赖配置
module example/project/app //模块路径
go 1.16 //标准库版本
require( //依赖单元
example/lib1 v1.0.2
example/lib2 v1.0.0
)
其中,一个依赖的标识由两部分组成:分别是模块路径和模块版本/伪版本
依赖配置的版本号
版本号可以分为两种:
-
语义化版本
形如x.y.z的版本号分别表示主版本,次版本号和修订版本号。
例如V1.3.0
-
基于commit的伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234版本前缀和语义化版本没有区别,后面的分别是时间戳和commit的hash。
依赖配置的关键字
- 对于没有直接导入的包(间接依赖)会使用
//indirect标识。
- 对于主版本号大于等于2的包,会在模块路径后增加
vN关键字,例如example/lib1/v3 - 对于没有go.mod文件且主版本大于等于2的依赖,会使用
+incompatible关键字
依赖版本的选择
会选择最低的兼容版本。即例如A程序依赖与B和C,而B和C分别依赖于D1.2和D1.3,那么最终会使用D1.3来进行编译。
依赖分发-回源/Proxy
使用Github等平台:
- 无法保证构建的稳定性
- 无法保证依赖的可用性
- 增加第三方压力
使用Proxy来保证依赖的稳定性,Proxy会缓存源站的代码库。
GOPROXY="https://proxy1.cn,https://proxy2.cn, direct"
如上配置的GoProxy拉取依赖的顺序为 Proxy1 -> Proxy2 -> Direct
go get
go get example.org/pkg
可以指定拉取特定的版本或者移除依赖。
go mod
go mod命令:
-
go mod init
初始化项目,创建go.mod文件
-
go mod download
下载模块到本地缓存
-
go mod tidy
增加需要的依赖,移除不需要的依赖
测试
Go单元测试
Go单元测试的基本规则
-
所有的单元测试文件以
_test.go结尾 -
所有的测试函数使用
func TestXxx(t *testing.T)来定义 -
初始化的逻辑放到
func TestMain(m *testing.M)函数中func TestingMain(m *testing.M){ //初始化 code := m.Run() // finalizing os.Exit(code) }
使用第三方的assert包
import "github.com/stretchr/testify/assert"
可以进行一些条件的判断,如assert.Equal等。
单元测试覆盖率评估
使用go test xxx_test.go xxx.go --cover来输出一个单元测试的覆盖率。
关于单元测试覆盖率的一些建议:
- 一般的覆盖率在50%~60%之间,较高的可以到达80%
- 测试分支应相互独立,全面覆盖
- 测试单元的粒度需要足够小,函数满足单一职责原则
单元测试依赖与Mock
单元测试的幂等性与稳定性
对于一个复杂项目,其组件可能依赖于文件、DB或者缓存系统。
在有外部依赖的情况下,实现单元测试的幂等性和稳定性,需要用到Mock机制。
使用Mock包
有很多的Mock包,例如monkey
它可以为一个函数打桩,通过反射机制使用自定义的函数替换掉目标函数。
monkey.Patch(ReadFirstLine, func() string{
return "line00"
})
//可以将ReadFirstLine函数替换成自定义的实现
defer monkey.Unpatch(ReadFirstLine)
//复原
基准测试
即性能测试,写法类似于单元测试
- 基准测试函数为
func BenchmarkXxx(b *testing.B) - 通过
b.ResetTimer()重置时间计数,可以去掉用于初始化的时间
标准库的rand底层实现中使用了锁,在对随机序列一致性要求不高的场合可以使用fastrand提升速度
项目开发流程简介
-
需求描述
-
进行用例分析(UML用例图)
-
定义实体关系ER图
-
项目的分层结构:
- 数据层:关联到数据模型,封装外部数据的增删改查
- 逻辑层:业务实体,处理核心业务逻辑输出
- 视图层:处理和外部的交互逻辑
开发示例:
go mod init go get gopkg.in/gin-gonic/gin.v1@1.3.0