这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go语言进阶 - 工程进阶
Go语言进阶和依赖管理
1.1 Goroutine协程
go语言里面的协程不等于线程
- 协程属于用户态,线程属于内核态
- 协程是轻量级线程,线程可以跑多个协程
- 协程的内存占用是KB级别,线程是MB级别
- 协程的创建和销毁的开销更小
在go语言里建立一个协程只需要在func前加一个go即可
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
1.2 CSP
CSP 并发模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享 channel(管道)进行通信的并发模型。
Go语言就是借用 CSP 并发模型的一些概念为之实现并发的,但是Go语言并没有完全实现了 CSP 并发模型的所有理论,仅仅是实现了 process 和 channel 这两个概念。
process 就是Go语言中的 goroutine,每个 goroutine 之间是通过 channel 通讯来实现数据共享。
go希望协程是通过通信来共享内存,而不是通过共享内存来通信
1.3 channel
如何建立一个channel呢
//建立channel方式
make(chan int)//无缓冲通道
make(chan int,2)//有缓冲通道
//A子协程发送数字
//B子协程计算平方和
//主协程输出平方和
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
//复杂操作
println(i)
}
}
无缓冲通道----放一个拿一个(同步通信)
有缓冲通道----放满然后等拿(生产消费),一般用于生产比消费速度慢的情况下,可以缓解一下消费速度和生产速度矛盾的问题
1.4 Lock
因为会出现多个协程对同一个资源操作的情况,和其他语言,go的基本库提供了锁
官方原文说低级同步才用互斥锁这种
用法比较简单,和其他语言差不多
var lock sync.Mutex
lock.Lock()
lock.Unlock()
1.5 waitGroup
跟Java的CountDownLatch差不多
waitGroup有几个方法
Add(delta int) 表示计数器+delta
Done() 计数器-1
Wait() 阻塞直到计数器为0
依赖管理
2.1 Go 依赖管理推进
2.1.1 GOPATH
GOPATH工作目录可以理解为每个人有一套按照标准格式的目录。
GOPATH目录下有三个包
-
bin 存放项目编译的二进制文件
-
pkg 存放项目编译的中间产物
-
src 存放项目源码
项目代码直接依赖src的代码
go get直接下载最新版本的包到src目录下
但是有一个弊端就是,A项目和B项目依赖了同一个包,但是版本不同,B依赖的包可能没有对应的功能,这会导致A和B不能同时构建成功。
2.1.2 Go Vendor
Go Vendor其实就是将依赖的包,特指外部包,复制到当前工程下的vendor目录下,这样go build的时候,go会优先从vendor目录寻找依赖包。
但是会出现子包不兼容的情况,比如主项目依赖了A和B项目,A和B项目依赖了同一个包,但是版本兼容。而且无法控制版本
2.1.3 Go Module
通过go.mod文件管理依赖包版本
通过go get/go mod指令管理依赖包
2.2依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
2.3.1 依赖配置-go.mod
module github.com/Moonlight-Zhao/go-project-example //依赖管理单元
go 1.16 //原生库
require ( //单元依赖
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.8.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/ugorji/go v1.2.7 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/gin-gonic/gin.v1 v1.3.0
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.3.3 // indirect
gorm.io/gorm v1.23.4 // indirect
)
每一个单元依赖由module路径 + 版本号来唯一标识
2.3.2 依赖配置-version
版本号主要有两个版本规则
语义化版本
${MAJOR}.${MINOR}.${PATCH}
| 名称 | 含义 |
|---|---|
| MAJOR | 不同的MAJOR版本表示是不兼容的API。因此即使是同一个库,MAJOR版本不同也会被认为是不同的模块 |
| MINOR | 通常是新增函数或功能,向后兼容 |
| PATCH | 一般是修复bug |
Commit版本 每次提交 commit 后,Go 都会默认生成一个伪版本号:
v0.0.0-yyyymmddhhmmss-abcdefgh1234
如:v1.0.0-20220517152630-c38fb59326b7
| 名称 | 含义 |
|---|---|
| v0.0.0 | 版本前缀和语义化版本是一样的 |
| yyyymmddhhmmss | 时间戳,提交Commit的时间 |
| abcdefgh1234 | 校验码,包含12位的哈希前缀 |
2.3.3 依赖配置-indirect
这是一个特殊标识符,表示 go.mod 对应的当前 module 没有直接导入的包,也就是非直接依赖 (即间接依赖) 。
例如,一个依赖关系链为:A->B->C 。其中,A->B 是直接依赖;而 A->C 是间接依赖。
2.3.4 依赖配置-incompatible
incompatible也是一个特殊标识符。对于 MAJOR 主版本在 V2 及以上的模块,go.mod 会在模块路径增加 /vN 后缀 (如下图中 example/lib5/v3 v3.0.2 )。这能让 Go Module 按照不同的模块来处理同一个项目不同 MAJOR 主版本的依赖。
由于 Go Module 是在 Go 1.11 才实验性地引入,所以在这个更新提出之前,已经有一些仓库打上了 V2 或者更高版本的 tag 了。 为了兼容这部分仓库,对于没有 go.mod 文件并且 MAJOR 主版本在 V2 及以上的依赖,会在版本号后加上 +incompatible 后缀。表示可能会存在不兼容的源代码。
2.3.5 依赖配置-依赖图
Go 选择最低的兼容版本
2.3.6 依赖分发-回源
依赖分发,即依赖从何处下载、如何下载的问题。 Go 的依赖绝大部分托管在 GitHub 上。Go Module 系统中定义的依赖,最终都可以对应到 GitHub 中某一项目的特定提交 (commit) 或版本。 对于 go.mod 中定义的依赖,则直接可以从对应仓库中下载指定依赖,从而完成依赖分发。
弊端
直接使用 GitHub 仓库下载依赖存在一些问题:
-
首先,无法保证构建稳定性。代码作者可以直接在 GitHub 上增加/修改/删除软件版本。
-
无法保证依赖可用性。代码作者可以直接在 GitHub 上删除代码仓库,导致依赖不可用。
-
第三,如果所有人都直接从 GitHub 上获取依赖,会导致 GitHub 平台负载压力。 解决方案-Proxy
-
Go Proxy 就是解决上述问题的方案。Go Proxy 是一个服务站点,它会缓存 GitHub 中的代码内容,缓存的代码版本不会改变,并且在 GitHub 作者删除了代码之后也依然可用,从而实现了 “immutability” (不变性) 和 “available” (可用的) 的依赖分发。
-
使用 Go Proxy 后,构建时会直接从 Go Proxy 站点拉取依赖。如下图所示。
2.3.7工具
go get工具
go get example.org/pkg +...
go mod工具
| 指令 | 功能 |
|---|---|
| init | 初始化,创建go.mod文件 |
| download | 下载模块到本地缓存 |
| tidy | 增加需要的依赖,删除不需要的依赖 |
3 测试
3.1 单元测试
单元测试主要就是对模块内的函数进行功能测试。
3.1.1 规则
- 所有测试文件以_test.go结尾
- 函数符合func Testxxx(*testing.T)
- 初始化逻辑放到 TestMain 中
之后只需要将工程文件和测试文件放在一起,然后终端使用go test即可开始测试
此外,我们可以利用assert断言来简单地测试。
代码覆盖率 指的就是源代码被测试的比例和程度,一般来说,代码覆盖率越高越好
3.2 Mock测试
mock主要用来操作预期数值不依赖于本地文件,就是我们可以手动给这个函数让它返回固定的值,从而实现测试效果。
一般操作是mock打桩
因为我没有深究过,我感觉用monkey.patch()就是临时重写一个方法,让它返回我们想要的值。
3.3 基准测试
基准测试和单元测试差不多
- 基准测试的函数必须以 Benchmark 开头,必须是可以导出的
- 入参是*testing.B
- 基准测试函数不能有返回值
InitServerIndex()是为了初始化服务器索引,初始化后我们需要重置时间,因为这一部分不是测试所花的时间。
之后一个Gin框架的案例我就不记录了,后续课程应该有详细讲解。
总结一下
今天的课主要关于go语言的一些协程知识,依赖管理以及一些测试方法。学到了之前没有学过的一些测试方法,受益匪浅。