go语言进阶&依赖管理 | 青训营笔记
这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
语言进阶
从并发编程的视角了解Go高性能的本质
Goroutine
Go可以充分发挥多核优势,高效运行
- 协程:用户态,轻量级线程,栈MB级别
- 线程:内核态,线程跑多个协程,栈KB级别
总的来说协程比线程轻量很多,而go语言会自己完成协程的创建和调度。
创建协程
在调用函数前面加上go关键字即可调用协程
快速打印helloworld示例:
func hello(i int) {
println("hello goroutine: " + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
/********************
hello goroutine: 0
hello goroutine: 2
hello goroutine: 3
hello goroutine: 4
hello goroutine: 1
*********************/
可以看到,输出是乱序的,也就是并行输出。
CSP
提倡通过通信共享内存(通道channel)而非通过共享内存(临界区)实现通信。
后者需要mutex进行互斥和加锁,在一定程度上会影响程序的性能
Channel
通过通信实现共享内存
创建命令:make(chan 元素类型, [缓冲大小])
- 无缓冲通道
make(chan int) - 有缓冲通道
make(chan int, 2)
无缓冲通道又被称为同步通道,缓冲通道就相当于暂存区(缺氧里面的储气罐)
下面看一个简单的生产者-消费者模型。
func CallSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
//A子协程发送0-9
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
//B子协程计算输入数字的平方
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
//主协程输出最后的平方数
println(i)
}
}
src和dest的传递能保证并发安全。
由于消费的速度可能比生产慢点,所以开了点缓冲。这样就不会因为消费者而影响生产者的执行效率。
也就是说,缓冲区的出现可以解决生产者消费者效率不匹配进而引发的问题。也很好理解,缺氧的自循环厕所中,芦苇就起到了缓冲区的作用。生产氧气的储气罐也是。
并发安全Lock
通过共享内存实现通信。没啥好讲的,要对共享的变量进行处理时一定要加锁,否则就会出现问题。之前写Springboot的时候已经碰到过了。
WaitGroup
WaitGroup是sync包下的一个东西,有这么些方法
Add(delta int):计数器加deltaDone():计数器减1Wait():阻塞直到计数器为0
可以这么用:开启子协程+1,执行结束-1,主协程阻塞直到计数器为0。
利用这个玩意就可以把之前的代码修改一下成这样
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()
}
小结
- 协程。go语言能通过高效的调度模型实现高并发操作
- 通道。协程间通过通信实现共享内存
- WaitGroup。可以实现并发安全操作和协程间的同步
依赖管理
依赖指的就是各种开发包。
Go依赖管理演进为GOPATH -> Go Vendor -> Go Module。
现如今基本用Go Module管理依赖
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
依赖管理三要素:
就像maven一样。
- 配置文件,描述依赖
go.mod - 中心仓库管理依赖库
Proxy - 本地工具
go get/mod
配置文件
以下是一个例子
module example/project/app //依赖管理基本单元
go 1.16 //原生库
require (
example/lib1 v1.0.2
example/lib2 v1.0.0 //indirect
example/lib3 v0.1.0-xxxxx-xxxx
example/lib4 v0.0.0-xxxx-xxxx //indirect
example/lib5/v3 v3.0.2
example lib6/v3.2.0+incompatible
)
version
- 语义化版本,如V2.3.4,V3.4.5
- 基于commit伪版本,vX.0.0-时间戳-哈希码前缀
indirect
假设A依赖了B,B依赖了C,那么A与B是直接依赖,A与C是间接依赖。
go对于A与C的这种情况都会在注释里打上indirect,表示没有直接导入该依赖模块的包。
incompatible
- 主版本2+模块会在模块路径增加
/vN后缀 - 对于没有
go.mod文件并且主版本2+的依赖,会+incompatible
没咋听懂,据说是为了兼容性。go会根据一定的算法,选择最低的兼容版本。
中心仓库管理依赖库
首先介绍一个概念叫依赖分发。依赖分发就是从哪里下载如何下载的问题。
假设直接从各个代码托管平台下载,会出现一些问题。比如,无法保证构建稳定性(增加/修改/删除软件版本),无法保证依赖可用性(删除软件),另外还会增加第三行代码托管平台的压力。
为了解决这个问题,Proxy就诞生了。Go Proxy是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用。使用Go Proxy后,构建时会直接从Go Proxy站点拉去依赖。
GOPROXY
GOPROXY是一个Go Proxy站点URL列表,可以使用“direct”表示源站。对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,去proxy2;如果proxy2不存在则会回源到源站直接下载依赖,缓存到proxy站点中。
就像cdn一样。
本地工具
go get
go get example.org/pkg默认情况下会直接下载最新版本。一些参数:
- @update, 默认
- @none, 删除依赖
- @v1.1.2, 特定的tag版本(语义)
- @24asdf5, 特定的commit
- @master, 分支最新的commit
go mod
- init,初始化然后创建一个go.mod文件
- download,下载模块到本地缓存
- tidy,增加需要的依赖,删除不需要的依赖