01 语言进阶
01 并发 vs 并行
并发指的是多线程程序在一个核的cpu上运行,本质上是通过线程的不断切换了同时运行。
并行指的是多线程程序在多个核的cpu上同时运行。
Go可以充分发挥多核优势,高效运行
1.1 Goroutine 协程
协程是轻量级的执行线程,用户态,栈内存是MB级别;
线程是内核态,可以跑多个协程,栈内存是KB级别。
在Go语言中,每一个并发的执行单元叫作一个goroutine。
在函数名字前面添加go
关键字表示为该函数创建一个协程运行。
快速打印hello0~4 => 开启多个协程打印
在代码里表示将i
作为参数传递给j
。
time.Sleep(time.Second)
目的是做堵塞,使得子线程未结束的时候主线程也不会退出。
go func(j int) {
hello(j)
}(i)
1.2 CSP-Communicating Sequential Processes
提倡通过通信共享内存而不是通过共享内存而实现通信
- 通过通信共享内存:通道将协程做了连接,相当于传输队列,能够保证收发数据的顺序。可以让A协程发送特定的值到B协程的通信机制。
- 通过共享内存而实现通信:通过互斥量对共享内存进行加速,容易影响程序性能。所以提倡第一种
1.3 Channel 通道
创建方式说明:make(chan type,[num])
其中:type表示元素类型,num表示缓冲大小,即能存放的元素个数
分类:
- 无缓冲通道(num=0)
make(chan int)
,也被称为同步通道。因为会导致发送的协程和接收的协程同步化。 - 有缓冲通道
make(chan int,2)
。解决了上述通道的缺点,典型的生产消费模型
课堂小demo
A子协程发送0~9数字,B子协程计算输入数字的平方,主协程输出最后的平方数
src
为无缓冲通道,dest
是有缓冲通道,缓冲大小为3
why把dest作为有缓冲通道
考虑到消费者也就是主协程可能会做一些复杂的操作,而生产者的逻辑较为简单,消费者的消费速度稍微慢一点,所以用带缓冲的队列就不会因为消费者的消费速度影响生产者的执行效率。解决消费者的消费速度和生产者的生产速度不匹配的问题
defer close(src)
表示延迟资源关闭
src <- i
表示把i放到通道src里
1.4 并发安全Lock
课程demo:对变量执行2000次+1操作,5个协程并发执行
package concurrence
import (
"sync"
"time"
)
var(
x int64
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
}
}
func Add(){
x=0
for i:=0;i<5;i++{
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock",x)
x=0
for i:=0;i<5;i++{
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock",x)
}
运行结果
WithLock 10000
WithoutLock 7013
在实际情况下,应当避免对共享内存做一些非并发安全的读写操作
1.5 WaitGroup
之前的代码都使用了Sleep
来实现阻塞,因为不知道子协程确切的执行时间,因此无法确切的设立sleep实现。
通过waitgroup来实现并发任务的同步,内部原理是维护一个计数器。当开启协程+1,执行结束-1,主协程阻塞直到计数器为0
三个主要方法:
- Add(delta) 表示计数器+delta
- Done()表示计数器-1
- Wait()表示阻塞直到计数器为0
使用waitgroup修改1.1中代码
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
02 依赖管理
2.1 Go依赖管理演进
普通的简单工程只需要依赖原生的SDK,但是实际的工程很复杂,不可能基于标准库0-1编码搭建,需要引入外部的依赖,这时候管理依赖库就很重要。
Go的依赖管理主要经历了GOPATH,Go Vendor跟Go Module三个阶段。演进主要围绕着不同环境依赖的版本不同以及需要控制依赖库的版本。
2.1.1 GOPATH
环境变量GOPATH
目录有以下结构
- bin 项目编译的二进制文件
- pkg 项目编译的中间产物,加速编译
- src 项目源码
项目代码直接依赖src下的代码,并且go get下载最新版本的包到src目录下
弊端
场景:
同一个package下,有两个版本,A变成了A1,B变成了B1,但是src只有一个版本存在,那AB项目无法保证编译通过。如果多个项目依赖同一个库,那么依赖库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这样就无法实现package的多版本控制
2.1.2 Go Vendor
在项目目录下增加vendor,所有依赖包副本形式都存在vendor里,优先使用vendor下的依赖,再从GOPATH里寻找。
弊端:
如果项目A依赖packageB和packageC,B和C依赖了D的不同版本,这两个版本可能会出现冲突,导致编译出错。
2.1.3 Go Module
通过go.mod文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
2.2 依赖管理三要素
- 配置文件,描述依赖:go.mod
- 中心仓库管理依赖库 proxy
- 本地工具:go get/go mod
2.2.1 go.mod
比如下面的代码块里,第一行是依赖管理基本单元,第二行声明了原生库,下面require里是单元依赖。
module github.com/Moonlight-Zhao/go-project-example
go 1.16
require (
github.com/gin-contrib/sse v0.1.0 // indirect
)
2.2.2 依赖配置 version
分为语义化版本和基于commit的伪版本
2.2.3 依赖配置 indirect
表示间接依赖
2.2.4 依赖配置 incompatible
主版本2+模块会在模块路径增加/vN后缀
对于没有go.mod文件且主版本2+的依赖,会+incompatible
依赖图:每次都要选择最低的兼容版本
2.3.5 依赖分发:回源
依赖分发:从哪里下载,如何下载的问题
可以从Github和SVN下载依赖,但是存在多个问题。
- 无法保证构建稳定性
- 无法保证依赖可用性
- 增加第三方压力
解决方案:go proxy
Go Proxy是一个服务站点,它会缓存站中的内容并且不改变版本
配置环境变量GOPROXY,配置说明为proxy1,proxy2,direct
寻找依赖的时候,优先从proxy1下载依赖,如果proxy1不存在的话,从proxy2下载依赖,如果proxy2也不存在,从direct里下载,并且缓存到proxy里。
2.3.6 go get
go get example.org/pkg 参数
@update 默认@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd 特定的commit
@master 分支的最新commit
2.2.7 go mod
go mod 参数
init 初始化,创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的依赖
3.测试
回归测试=>集成测试=>单元测试
从左到右,覆盖率逐层变大,成本却逐层降低
3.1 单元测试
单元测试主要是输入、测试单元、输出以及校对。
测试单元里包括函数、模块等。
目的是保证质量和提高效率。
3.1.1 规则
- 所有的测试文件以_test.go结尾
- func Testxx(*testing.T)
- 初始化逻辑放到TestMain中
3.1.2 覆盖率
通过覆盖率来判断是否达到要求。
一般覆盖率都在50%~60%,较高覆盖率到80%
测试分支相互独立、全面覆盖
测试单元粒度足够小、函数单一职责
测试要保证稳定性和幂等性
3.2 Mock测试
快速Mock函数:为一个函数打桩、为一个方法打桩
打桩测试后就不再依赖本地文件
3.3 基准测试
优化代码,需要对当前代码进行分析
内置的测试框架提供了基准测试的能力