这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go语言进阶与依赖管理
01.语言进阶--并发编程
并发&并行
并发:多个线程的程序在一个核的cpu内运行,通过切换时间片来实现;
并行:多个线程的程序在多个核的cpu内运行,直接利用多核特点实现;
Go可以充分发挥多核优势,最大限度地利用计算资源,高效运行。
协程Goroutine&线程
线程:一种较为昂贵的系统资源,属于内核态,栈MB级别。它的创建、切换和暂停都需要特别大的计算开销,占用较大的计算资源。
协程:一种轻量级的系统资源,属于用户态,栈KB级别。可在一个线程内并发地运行多个协程。
Go语言可以一次开上万个协程。
如何在Go语言中开启一个协程?
调用函数时,在其函数名前添加关键字go即可
CSP
通过通信实现共享内存
如上图左,该机制涉及到一个重要概念——通道(channel),通道将若干个协程连接起来,数据的传输遵循着先入先出的顺序。
通过共享内存实现通信
如上图右,该机制通过使多个进程映射至同一片缓存空间实现共享内存和数据交换。其存在的问题是,当映射至同一片缓存控件的多个进程进行切换时,有可能打断上一个进程的工作,从而互相干扰,导致数据错误。
例如:A进程和B进程共享同一片缓存空间。当A进程书写数据至一半时,B进程进入,cpu切换至B进程,A进程因此被打断。由于A、B进程内存共享,故B进程将在A进程的基础上继续写入数据。当A进程恢复运行时,此时内存上已写入B进程的数据,再写入则造成数据错误。
Channel
channel是一个引用类型,通过make方法创建。
make(chan 元素类型,[缓冲大小])
第一个参数设定通道内储存的数据类型,第二个参数设定通道内数据的最大存储量,若无第二个参数,则通道为无缓冲通道,否则为有缓冲通道
关于缓冲通道的理解
协程A产生数据,并通过通道传递给协程B。有的时候,协程B对数据的处理操作较为复杂,所需时间增多,导致其处理数据的速度慢于协程A生产数据的速度,因而设置缓冲通道,缓解协程B的压力。类似快递驿站对快递的暂时收容。
demo
func CalSquare(){
src:=make(chan int)
dest:=make(chan int,3)
利用make方法构建两个通道
go func(){
defer close(src)
for i:=0;i<10;i++{
src ← i
}
}()
创建协程,利用for循环生成数据0-9,并传给通道src
go func(){
defer close(dest)
for i:=range src {
dest ← i * i
}
}()
创建协程,读取src通道内的数据,并传给通道dest
for i:= range dest{
println(i)
}
}
读取dest通道内的数据并打印,若此步主协程的操作较为复杂,消耗时间较多,导致读取数据的速度慢于子协程发送数据的速度,可能产生错误,但由于dest通道设置了缓冲通道,降低了此错误发生的可能
并发安全 Lock
Go中保留了通过共享内存实现通信的机制,如前所述,此机制容易引起数据错误,需要通过加锁Lock进行保护,下面是加锁和不加锁的小测试
如上图左,addWithLock方法通过Lock()和Unlock()对临界区进行保护,输出结果是期望的10000,而addWithoutLock方法的输出结果是一个未知的数字。
WaitGroup
可以实现并发任务的同步 其内部有三个方法Add(delta int)、Done()、Wait(),用来维护计数器。当并发任务启动时,调用Add方法将计数器值设为任务数,每执行完一个任务就调用一次Done方法,使计数器值减一,Wait方法则将一直阻塞主协程,直至计数器值为0,由此实现并发任务的同步。
func ManyGoWait(){
var wg sync.WaitGroup //创建WaitGroup
wg.Add(5) //将计数器值设为5
for i:=0;i<5;i++{
go func(j int){ //创建子协程(即一个并发任务)
defer wg.Done()//计数器值-1
hello(j) //打印
}(i)
}
wg.Wait() //阻塞
}
02.依赖管理
Go的依赖管理方案演进历经了GOPATH、Go Vendor、Go Module三个阶段,围绕着依赖不同版本和依赖版本更新两个问题而不断改进至现在的Go Module方案。
GOPATH
GOPATH是Go的一个环境变量,其下有三个文件夹。bin存放项目编译的二进制文件,pkg存放项目编译的中间产物,src存放项目源码。go get下载最新版本的包到src下,所有项目都将依赖src下的代码。
由此带来的弊端是无法实现不同项目依赖同一package的不同版本,若该package的较高版本不兼容较低版本,则将导致多个项目无法同时成功运行。
Go Vendor
为解决GOPATH弊端,Go Vendor方案在每个项目的目录下新增了vendor文件,存放依赖包副本。依赖寻址的方式为优先查找vendor,查无则查找GOPATH。通过每个项目引入一份依赖的副本问题,解决了多个项目需要同一个package依赖的冲突问题。
该方案同样存在弊端,当一个项目间接依赖同一package的不同版本时,无法控制依赖的版本,在不同版本不兼容时,编译出错
Go Module
Go Module通过go.mod文件管理依赖包版本,通过go get/go mod指令工具管理依赖包,能够定义版本规则和管理项目依赖关系。
依赖管理的三要素:
(1)描述依赖的配置文件---go.mod
依赖配置-version
①语义化版本
{MAJOR}.{MINOR).{PATCH}
例:V1.3.0、V2.3.0
②基于commit 伪版本
例:vx.0.0-yyyymmddhhmmss-abcdefgh1234
此外,require单元内还会有indirect关键字和incompatible关键字加以描述,前者表示间接依赖(当A依赖B,B依赖C时,A间接依赖C),后者表示没有go.mod文件并且主版本2+的依赖。
(注:C 1.4兼容C 1.3)
答案:B 选择最低的兼容版本
(2)管理依赖库的中心仓库---Proxy
缓存第三方原站中所依赖的代码,稳定可靠。寻找依赖地址时首先寻找Proxy,再寻找第三方站点。
(3)本地工具---go get/mod