这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
并发
协程Goroutine
概念区分
进程/线程
-
进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
-
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。
并发/并行
- 多线程程序在单核心的 CPU 上运行,称为并发;
- 多线程程序在多核心的 cpu 上运行,称为并行。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
线程/协程
-
线程:一个线程上可以跑多个协程,内核态,栈MB级别。
-
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程;用户态,轻量级的线程,栈KB级别。
优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。
Go创建Goroutine
在函数调用前使用go关键字
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
Go中通信机制Channel
Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。”
channel类型
make(chan 元素类型, [缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int, 2)
- 每次向 channel 发送数据时,都会将元素添加到队列中。 然后,接收操作将从队列中删除该元素。 当 channel 已满时,任何发送操作都将等待,直到有空间保存数据。 相反,如果 channel 是空的且存在读取操作,程序则会被阻止,直到有数据要读取。
func CalSquare() {
src := make(chan int) //无缓冲
dest := make(chan int, 3) //有缓冲
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i /*将i发送到src协程*/
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i /*计算并将结果发送到dest协程*/
}
}()
for i := range dest { //主协程遍历
//复杂操作
println(i)
}
}
并发安全Sync包
Lock通过对临界区的权限的控制保证并发安全
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
//每次+= 1前获取临界区权限
lock.Lock()
x += 1
//释放临界区权限
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
//无保护
x += 1
}
}
因为并发进行,所以无法预测执行先后顺序,可能发生数据竞争,应避免对共享内存进行非并发安全的读写操作
避免数据竞争的三种方法:
1.不要写变量
2.避免从多个goroutine访问变量
3.允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问
WaitGroup
通过计数器控制阻塞线程
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
依赖管理
依赖管理目标:1.不同环境(项目)依赖的版本不同; 2.控制依赖库的版本
Go依赖管理演进
环境变量$GOPATH
GOPATH对应的的工作区目录有三个子目录:
- bin:保存编译后的可执行程序
- pkg:保存编译后的包的目标文件
- src:存储源代码
项目代码依赖src下的代码;包位于src目录下
GOPATH弊端:无法实现package的多版本控制
Go Vendor
- 在项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor优先于GOPATH
通过每个项目引入一份依赖的副本,解决多项目需要同一个package的问题
Go Vendor弊端:1.无法控制依赖的版本;2.更新项目可能出现依赖冲突,导致编译出错
Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
目标:定义版本规则和项目依赖关系
依赖管理三要数:
1.配置文件,描述依赖 go.mod
2.中心仓库管理依赖库 Proxy
3.本地工具 go get/go mod
依赖配置
依赖配置原则:选择最低兼容版本
go.mod
version
indirect非直接依赖
incompatible
主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。由于gomodule是1。11实验性引入所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀
依赖分发
变量GOPROXY
GOPROXY是一个 Go Proxy 站点URL列表,可以使用“direct”表示源站。整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。
go get
go get example.org/pkg
在后面可以添加
- @update 默认, 拉最新的
- @none 删除依赖
- @v1.1.2 tag 版本, 语义版本
- @23dfdd 特定的 commit
- @master 特定的分支
go mod 后面添加
- init 初始化,创建go.mod文件
- download 下载模块到本地缓存
- tidy 增加需要的依赖,删除不需要的依赖
测试
回归测试,集成测试,单元测试
单元测试
规则:
- 所有文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
以代码覆盖率评估测试
tips:
- 一般覆盖率:50%~60% ; 较高覆盖率:80%
- 测试分支相互独立,全面覆盖
- 测试单元粒数足够小,函数单一职责
依赖:
Mock
- 为一个函数打桩
- 为一个方法打桩