这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天.
GO语言进阶
从并发编程的视角了解GO高性能的本质。
并发&并行
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
Goroutine
Goroutine的本质是协程,是实现并行计算的核心。
- 线程是比较昂贵的系统资源,属于内核态,他的创建,切换,停止都属于比较重的系统操作,比较消耗资源(MB级别)。
- 协程是轻量级,用户级的线程,创建和调度由go语言本身去完成,比线程轻量。在一个线程中可以并发去跑多个协程,协程栈在KB级别,go语言可以一次创建上万的协程。
协程代码示例:
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)
}
CSP协程间的通信(Communicating Sequential Processes)
GO提倡通过通信来共享内存;而不是通过共享内存来实现通信。Gorountine是程序并发的执行体,Channel把协程做了连接,像是传输队列,遵循先入先出顺序。但GO也保留了通过共享内存来实现通信,但在一定程度上影响程序性能。
Channel
Channel是引用类型,需通过make函数创建 "make(channel元素类型,[缓冲大小])",根据缓冲大小可分为无缓冲通道"make(chan int)"和有缓冲通道"make(chan int, 2)"。
当消费者的消费速度比生产者生产速度慢时,使用带缓冲的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
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
并发安全LOCK
需求:对变量执行2000次+1操作,5个协程并发执行。若不加锁,输出结果不会是10000.
示例:
WaitGroup
计数器:开启协程+1;执行结束-1;主协程阻塞直到计数器为0。改进代码如下:
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func ManyGo() {
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()
}
依赖管理
在做复杂项目时,项目涉及的framework,driver,log,collection等等都可以通过sdk的方式引入,因此对依赖包的管理就十分重要。GO依赖管理发展史:GOPATH->Go Vendor->Go Module,目前常用为Go Module。
Go Path
环境变量$GOPATH下有三个目录:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,用于提升编译速度
- src:项目源码
项目代码直接依赖src目录下的项目源码;go get下载最新版本的包到src目录下。
弊端:
当两个项目依赖于某一package的不同版本时,无法实现package的多版本控制(项目A用到了pkg V1的f1函数;迭代时项目B将pkg V1中f1函数删除,升级成为pkg V2的f2函数,导致项目AB无法都能编译通过)。所以,在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,无法满足我们的项目依赖需求。
Go Vendor
Go Vendor则在当前项目中创建一个目录,其中存放当前项目依赖的副本。通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
弊端:
但Vendor无法很好解决依赖包的版本变动问题,以及一个项目依赖同一个包的不同版本问题,例如:如图项目A依赖Pkg B和C,而B和C依赖了D的不同版本,通过Vendor的管理模式我们不能很好的控制对于D的依赖版本,一旦更新项目,有可能带来一来冲突。因此,Vnedor不能很清晰的标识依赖的版本概念。引出了Go Module。
Go Module
定义版本规则和管理项目依赖关系,解决了前两种依赖管理系统存在的无法依赖同一个库的多个版本等问题。
- go.mod:配置文件,描述依赖,管理依赖包版本。
- Proxy:中心仓库管理依赖库
- go get/go mod:本地指令工具管理依赖包。
依赖配置-go.mod
go.mod由三部分组成:依赖管理基本单元、原生库、单元依赖。
- 依赖管理基本单元:模块路径用来标识一个模块,从该模块可以看出从哪里找到该模块。例如github前缀表示可以从github仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的go.mod init文件进行管理。
- 原生库:依赖的原生sdk版本
- 单元依赖:依赖标识:[Module Path][Version/Pseudo-version]每个依赖单元用模块路径+版本来唯一标识。
Version
GOPATH和Vendor都是源码副本方式依赖,没有版本概念,而gomod为了方便管理则定义了版本规则,分为语义化版本,和基于commit伪版本。
语义化版本包括major(不同的major版本表示是不兼容的API,即使是同一个库,major版本不同也会被认为是不同的模块),minor(新增函数或方法,向后兼容),patch(修复bug)。
基于commit版本包括基础版本前缀;时间戳(提交commit的时间);校验码。
Indirect
indirect后缀表示go.mod对应的当前模块没有直接导入该依赖模块的包,也就是非直接依赖,表示间接依赖。例如:
Incompatible
如果major版本号大于 1 时,其版本号还需要体现在 Module 名字中。例如,Module github.com/rrr/m, 如果版本号增长到v2.x.x时,其module名字也需要相应改变为:github.com/rrr/m/v2。如果module的版本号虽然变成v2.x.x,但module名字没变,我们称这个module为不规范的module,为了加以区分,go命令会在go.mod中增加+incompatible标识(github.com/rrr/m/ v2.x.x+incompatible)。
依赖图
选择最低兼容的版本:V1.4(包含了V1.3)。
回源
Gomodule的依赖分发问题就是问从哪里下载依赖包和如何下载。但是直接使用版本管理仓库下载依赖存在多个问题:
- 无法保证构建稳定性(增加、修改、删除软件版本)
- 无法保证依赖可用性(删除软件)
- 增加第三方代码托管平台压力
Go Proxy就是解决这些问题的方案,Go Proxy是一个服务站点,他会缓冲源站中的软件内容,因为缓冲版本不会改变且子啊源文件删除后依然可用,所以实现了“immutability”和“available”的依赖分发。在项目中,如果下游无法满足上游需求,可以使用proxy。
变量GOPROXY
Go Mod通过GOPROXY环境变量控制如何使用Go Proxy;GOPROXY是一个Go Proxy站点的URL列表,可以使用“direct”表示源站。图例中,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,再到proxy2寻找,如果proxy2中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。
工具-go get
Go module的管理工具之一是go get,示例:go get example.org/pkg。后面参数为:
- @update 默认
- @none 删除依赖
- @v1.1.2 tag版本,语义版本
- @23dfdd5 特定的commit
- @master 分支的最新commit
工具-go mod
Go module的另一个管理工具是go mod,示例:go mod。后面参数为:
- init 初始化,创建go.mod文件
- download 下载模块到本地缓存
- tidy 增加需要的依赖,删除不需要的依赖
提交前尽量执行go tidy,减少构建时无效依赖包的拉取。
总结
Go语言依赖管理的三要素为go.mod配置文件,描述依赖、proxy中心仓库管理依赖库、go get/mod 本地工具。