Go语言进阶与依赖管理
语言进阶
并发 VS 并行
并发是多线程程序在一个核的cpu上运行
操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
并行是多线程程序在多个核的cpu上运行
Go可以发挥多核优势,高效运行
进程、线程、协程
进程:对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
线程:在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的“子任务”称为线程(Thread)。
协程:轻量级线程。
用户态和内核态
用户态线程:用户自己创建、管理和销毁的线程
内核态线程:运行在内核中,由内核和操作系统调度
Goroutine
协程:用户态,轻量级线程,栈KB级别
线程:内核态,线程跑多个协程,栈MB级别
创建协程
快速打印hello goroutine:0 ~ hello goroutine:4
func hello(i int) {
println("hello goroutine :" + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0;i < 5;i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
创建协程:
go func(形参) {
函数(形参)
}(实参)
输出结果:
hello goroutine :0
hello goroutine :4
hello goroutine :1
hello goroutine :3
hello goroutine :2
结果不一定按顺序输出。
CSP(Communicating Sequential Processes)
协程之间可以通过通信共享内存,也可以通过共享内存实现通信。
Channel
Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。
创建通道:
make (chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2) 解决数据同步问题
简单应用:
A 子协程发送0~9数字
B 子协程计算输入数字的平方
主协程输出最后的平方数
代码如下:
func CalSquare() {
chan1 := make(chan int)
chan2 := make(chan int, 3)
//子协程A
go func() {
defer close(chan1)
for i := 0; i < 10; i++ {
chan1 <- i
}
}()
//子协程B
go func() {
defer close(chan2)
for i := 0; i < 10; i++ {
chan2 <- i * i
}
}()
//主协程
for i := range chan2 {
//复杂操作
println(i)
}
}
chan2带缓冲的原因:
子协程B可能比子协程A慢,为了解决协程之间速度不匹配和避免数据传输混乱的问题,通道2带缓冲,类似Cache的功能。
defer:通道延迟关闭
输出结果:
0
1
4
9
16
25
36
49
64
81
并发安全 - Lock
当多个goroutine对内存同时进行访问时,为了避免内存资源使用的混乱,可以使用互斥锁实现并发程序对公共资源访问的限制。
下面是通过一个例子看到互斥锁的作用:
对变量执行2000次+1操作,5个协程并发执行
代码如下:
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 addwithoutLock()
}
time.Sleep(time.Second)
println("Withoutlock:", x)
x = 0
for i := 0; i < 5; i++ {
go addwithLock()
}
time.Sleep(time.Second)
println("Withlock:", x)
}
输出结果:
Withoutlock: 8104
Withlock: 10000
可以看到,如果不把临界区上锁的话会造成内存资源管理混乱,结果与预期不符;但执行了mutex.Lock()操作后,如果有另外一个 goroutine 又执行了上锁操作,那么该操作会被阻塞,直到该互斥锁恢复到解锁状态。
注意:通过go关键字开启一个协程,执行匿名函数里面的内容,这里需要注意主协程需要休眠一会儿,以便等开启的协程执行完,这是因为go中只要main函数线程退出则协程就退出。
并发同步 - WaitGroup
WaitGroup:
1、Add(delta int):计数器+delta
2、Done():计数器-1
3、Wait():阻塞直到计数器为0
计数器
开启协程+1;执行结束-1;主协程阻塞直到计数器为0。
改进:快速打印hello goroutine:0 ~ hello goroutine:4
func ManyGoWait() {
//创建同步器
var wg sync.WaitGroup
//5个信号
wg.Add(5)
for i := 0; i < 5; i++ {
//开启一个协程,执行匿名函数里面的内容
go func(j int) {
//执行完1个协程,信号量-1
defer wg.Done()
hello(j)
}(i)
}
//阻塞,等待所有协程执行结束
wg.Wait()
}
输出结果:
hello goroutine :4
hello goroutine :3
hello goroutine :2
hello goroutine :0
hello goroutine :1
依赖管理
Go依赖管理演进
GOPATH->Go Vendor->Go Module
GOPATH
环境变量:$GOPATH
- bin:项目编译的二进制代码文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码
项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
GOPATH依赖管理的问题:
假如使用基于GOPATH的依赖管理机制,你创建了一个Go程序,在写该程序的时候引入了依赖D,你使用命令go get获取到了依赖D的最新版本1.0.1(因为基于GOPATH的依赖机制没有版本感知的,所以就会拉取最新版本),成功的将应用运行起来了。
过了一段时间,要添加新的功能,此时你又需要依赖C,所以你再次使用go get下载下来了C的最新版本1.8,完成之后,你开心的点击运行,结果程序突然崩溃了,花了一段时间终于解决了问题。
问题就是在C中也依赖了D,但是在你执行go get C命令时,在本地找到了D,所以就不再去拉取D的其他版本了;而C代码中依赖的是D的版本v1.0.4,在这个版本解决了前面版本的一些bug,所以C可以正常使用,而现在c用的是D的v1.0.1版本,所以就出错了。
为了解决这个问题,你打算使用命令go get -u将依赖更新到最新版本,之后你又开心的点击了运行,发现还是出错了,一段时间后,你发现了在D的最新版本1.1.6又引入了一个Bug导致C不能正常工作;逐渐的你失去了耐心,这一切都是因为基于GOPATH依赖管理没有版本的概念。
问题:无法实现package的多版本控制
Go Vendor
在项目目录下增加vendor目录,所有依赖包副本形式放在$ProjectRoot/vendor
依赖寻址方式:
当编译程序时首先会在当前项目的vendor目录下去查找依赖如果找不到才会去$GOPATH/src下面去找;vendor是这个项目独有的依赖,而$GOPATH/src是当前$GOPATH下面多个项目所共享的依赖。
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖冲突的问题。
Go Vendor弊端:
Go Module
通过go.mod文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
依赖管理三要素
1、配置文件,描述依赖 go.mod
2、中心仓库管理依赖库 Proxy
3、本地工具 go get/mod
依赖配置 - go.mod
module $ProjectRoot //依赖管理基本单元
go 1.20 //go的版本(原生库)
//单元依赖
require(
依赖标识:[Module Path][Version/Pseudo-version]
)
依赖配置 - version
语义化版本
${MAJOR}.${MINOR}.${PATCH}
MAJOR相同,MINOR前后版本可以实现兼容,PATCH只是修复一些错误。
基于commit伪版本
vx.0.0-yyyymmddhhmmss-abcdefgh1234
版本号-提交时间-12位哈希校验码
依赖配置 - indirect
A->B->C
A对B是直接依赖,A对C是间接依赖,会加上indirect关键字。
依赖配置 - incompatible
当前库的major版本大于2时需要在模块路径后增加/vN后缀,如果没有依赖会+incompatible,可以正常使用。
依赖配置 - 依赖图
根据最小版本选择算法,会选择最低的兼容版本。
依赖分发 - 回源
依赖分发:依赖包的来源,下载地址。
GitHub:对于go.mod中定义的依赖,可以直接从对应仓库中下载指定软件依赖,从而完成依赖分发。
缺陷:
(1)无法保证构建稳定性:增加/修改/删除软件版本
(2)无法保证依赖可用性:删除软件
(3)增加第三方压力:代码托管平台负载问题
依赖分发 - Proxy
Go Proxy是一个服务站点,它能从源站点直接拉取依赖并进行缓存,解决了稳定性和可用性的问题。
当我们创建项目时可以直接从Go Proxy中拉取依赖。
依赖分发 - 变量 GOPROXY
GOPROXY="proxy1.cn, proxy2.cn, direct"
服务站点URL列表,"direct"表示源站
拉取依赖顺序:proxy1->proxy2->direct
工具 - go get
go get example.org/pkg +
1、@update:默认
2、@none:删除依赖
3、@v1.1.2:tag版本,语义版本
4、@23dfdd5:特定的commit
5、@master:分支的最新commit
工具 - go mod
go mod +
1、init:初始化,创建go.mod文件
2、download:下载模块到本地缓存
3、tidy:增加需要的依赖,删除不需要的依赖