这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
并发操作
并发vs并行:
| 并发与并行 | |
|---|---|
| 并发 | 并行 |
| 多线程在一个CPU上运行 | 多线程在多核cpu上运行 |
并行一般是实现并发的手段,并发可以最大程度的调度cpu资源,而go语言可以充分发挥多核优势,可以说Go语言就是为并发而生的。
Goroutine
| 协程与线程 | |
|---|---|
| 协程 | 线程 |
| 用户态,轻量级线程,栈KB级别,耗资源少。 | 内核态,线程跑多个协程,栈MB级别。线程一般并发跑多个协程,最高可以支持跑几万个协程。 |
创建协程的方法: 在运行函数前面加go关键字 go func
func HelloGoRountine(){
for i:=0;i<5;i++{
go func(j int){
hello(j)
}(i)
}
time.Sleep(time.Second)
}
用sleep阻塞,保证子协程执行完之前,主协程不退出。但是这样的处理并不好,因为我们强行规定了sleep的时间,实际并不需要用这么多时间,还有更优秀的处理办法,也就是Waitgroup
Waitgroup:
比sleep更好的解决方式。Waitgroup本质上是计数器,开启协程时使用add协程数+1,结束用done提示协程数-1,阻塞器wait知道协程数为0才会关闭
通道channel
通道的声明使用make(chan 元素类型,[缓冲大小])
channel有两种:无缓冲通道与有缓冲通道。
无缓冲通道也被称为同步通道。同步输入输出。
解决同步问题的方法就是使用带有缓冲区的有缓冲通道,通道的容量代表通道中可以存储多少元素
并发安全Lock
在程序并发运行时,最好对临界区加锁保证程序的安全运行,否则可能输出错误的结果:
go语言提供了sync.MuteX来实现锁,其实就是操作系统中的互斥,防止同时对公共资源的调用导致错误
var lock sync.MuteX
func addWithLock(){
for i:= 0; i<2000; i++{
lock.Lock()
x+=1
lock.Unlock()
}
}
这样一段代码就实现了简单的锁。同样的下面是不加锁的代码
func addWithoutLock(){
for i:= 0; i<2000; i++{
x+=1
}
我们使用go addwithlock()与go addwithoutlock()并行的创建五个协程来运行两个函数
Addwithlock函数对临界区实现了保护,最终输出了10000。
而Addwithoutlock函数没有对临界区保护,最终的输出值是个随机值,可能是正确值,也可能是小于10000的错误值。这个错误是概率发生的,因此在项目开发中很难定位,所以要对临界区做好保护。
依赖管理:
由于不同环境(项目)依赖的版本不同,所以我们需要对依赖进行管理,依赖管理从GOPATH到Go Vendor以及最新的Go Module,功能越来越强大。
环境变量GOPATH
GOPATH是我们在安装go语言的运行环境时就配置的环境变量,GOPATH主要包含了三个文件夹:
| GOPATH | |
|---|---|
| bin | 项目编译的二进制文件 |
| pkg | 项目编译的中间产物,加速编译 |
| src | 项目源码 |
项目代码直接依赖src下的代码
GOPATH的弊端:
如果一个项目在迭代前后依赖某一package的不同版本,GOPATH无法实现package的多版本控制
Go vendor
相比GOPATH,Go vendor在项目目录下增加vendor文件夹,vendor文件夹中是项目依赖的副本,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
Go vendor弊端:
- 无法控制依赖的版本
- 更新项目有可能出现依赖冲突,导致编译出错
Go Module
最新版本
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包 类似Java中的maven
module example/project/app
go 1.16
require{
example/lib v1.0.2
example/lib2 v1.0.0 //indirect
example/lib3 v0.1.0-20190725025543-5a5fe074e612
example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd //indirect
example/lib5/v3 v3.0.2
example/lib6 v3.2.0 incompatible
}
第一行是模块路径,是依赖管理的基本单元,简单来说就是在哪里可以找到这个模块。
第二行是go原生库的版本。
Require是单元依赖
语义化版本
${MAJOR}.${MIINOR}.${PATCH}
例如:
- V1.3.0
- V2.3.1
MAJOR:大版本,不同的MAJOR版本代码隔离,可以不兼容
MINOR:小版本,一般是功能的增加,在MAJOR相同的情况下要做的代码兼容
PATCH:修复bug
Commit伪版本
vx.0.0-yyyymmddhhmmss-abcdergh1234
第一部分和语义化版本一致,第二部分是时间戳,第三部分是Go语言自动生成的版本号,是12位的哈希码
关键字
indirect
表示间接依赖,假如项目A依赖项目B,项目B依赖项目C,项目A对项目C就是间接依赖
incompatible
- 主版本2+模块会在模块路径增加/vN后缀
- 对于没有go.mod文件并且主版本2+的依赖,会+incompatible 以上面的go module为例,主版本是v1.0.2,对于lib5,它的版本是v3.0.2,所以要在后面加/v3
对于lib6,没有go.mod文件,且版本超过主版本2,所以要加incompatible
构建时会选择最低的兼容版本,因为1.3与1.4是MINOR版本迭代,所以代码兼容。
依赖分发:
依赖分发其实就是项目的依赖在哪里下载,如何下载
Proxy
因为我们使用的依赖可能被修改或者删除,这样就会对我们的项目造成影响。为了解决这个问题,就出现了Proxy,Proxy会缓存源站的内容,即使作者对依赖进行修改删除,也不会影响Proxy的内容。
GOPROXY也是一个环境变量,当我们使用proxy时,项目会首先去Proxy中寻找依赖,如果没找到就返回第三方源站
工具 go get/mod
go get命令
- go get example.org/pkg @update 默认,不写也可以
- go get example.org/pkg @none 删除依赖
- go get example.org/pkg @v1.1.2 tag版本,语义版本
- go get example.org/pkg @23dfdd5 特定的commit
- go get example.org/pkg @master 分支最新的commit
go mod命令
- go mod init:初始化,创建go.mod文件
- go mod download: 下载模块到本地缓存
- go mod tidy:增加需要的依赖,删除不需要的依赖
项目创建时就要用 go mod init初始化依赖
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
测试
- 回归测试
- 集成测试
- 单元测试 从上到下,覆盖率逐渐变大,成本却逐渐降低。
go语言提供了专门的测试方式:
- 所有的测试文件必须以_test.go结尾
- func TestXxx(*tesing.t)
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectoutput := "Tom"
if output != expectoutput {
t.Errorf("Error,The output %s don't match expectoutput %s", output, expectoutput)
}
}
测试例子:期望输出值是Tom,实际输出Jerry
测试输出结果:
单元测试的覆盖率
显示单元测试覆盖率的指令是:
go test 测试程序 被测试程序 --cover
执行后会在下方显示测试率。
上面这段代码的测试覆盖率是66.7%,这是因为我们测试了输入值为70的情况下是否会输出true,但是没有测试在输入值小于60的情况下输出是否为false。因此最后一行代码的正确性没有得到检测。2/3就是66.7%
单元测试Tips
- 一般覆盖率在50-60
- 测试分支相互独立,全面覆盖
- 测试单元粒度足够小,函数单一职责
单元测试要隔离,也就是说测试要能在任何时间,在任何单元独立运行
所以我们需要用到mock机制,举个例子:
比如我们写了一串代码,需要打开文本文件,使用scan遍历并返回字符串,最终使用replaceall函数将字符串中的00全部替换为11。这个操作有一个前提条件,本地有这个文本文件,如果这个文本文件被篡改或删除,或者不在本地,就无法进行了。
这里就要用到mock
monkey下载地址: github.com/bouk/monkey 我们可以使用快速mock函数对一个函数或者方法打桩。 语法为:
func Patch(target,replacement interface{}) *PatchGuard
target就是原函数,replacement是打桩函数,我们在实际测试时调用的其实是打桩函数