一. 语言进阶
1. 并发vs并行
并发
指的是多线程在一个核的cpu上运行
并行
指的是多线程在多个核的cpu上运行
GO可以充分发挥多核心优势,高效运行
2. Goroutine
线程属于内核态
,它的创建切换停止都是很重的系统操作,它的栈是MB级别的。内部可以跑多个协程
。
协程属于用户态
,可以类比为轻量级的线程
,协程的创建和操作由GO语言自身操作
。它的栈是KB级别的。这就是go语言更适合高并发的原因所在。
下面是一个例子:
快速
打印 hello goroutine:0~hello goroutine:4
要求快速需要开多个协程来完成。
在go语言中开启一个协程是十分简单的,只需要在调用函数时在前面加一个go就行了。
go hello(i)
最后的 time.Sleep(time.Second)
是为了保证在所有协程完成之前主协程不退出。
package main
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine:" + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
go hello(i)
}
time.Sleep(time.Second)
}
这个是输出的代码,可以看出是乱序。表示是并行输出的。
3.CSP(Communicatting Sequential Processes)
go语言提倡通过通信共享内存
而不是通过共享内存而实现通信。
通过通信共享内存
就需要通道,通道相当于把gorountine做了一个链接,类似传输队列遵循先入先出。
go语言也保留着通过共享内存而实现通信
,通过共享内存而实现通信必须有一个互斥量对临界区进行一个枷锁。需要获取临界资源的权限,在这种机制下不同的gorountine之间容易发生数据静态的问题,在一定程度上影响程序的性能。
4. Channel
Channel是一种引用关键字需要通过:make(chan 元素类型 ,缓冲大小)
,进行创建。
通过缓冲大小可以分为无缓冲通道(同步通道)
和有缓冲通道
。
下面是一个例子来看Channel的具体使用。
第一个子协程生产数字通过src这个无缓冲通道发给第二个子协程,第二个子协程计算输入数字的平方然后后发给主协程输出。
package main
func main() {
src := make(chan int)
dest := make(chan int, 2)
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)
}
}
5. 并发安全Lock
对变量执行2000次+1操作,10个携程同步执行
不加锁的数字会出错
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func main() {
x = 0
for i := 0; i < 10; i++ {
go addWi()
}
time.Sleep(time.Second)
println(x)
x = 0
for i := 0; i < 10; i++ {
go addWio()
}
time.Sleep(time.Second)
println(x)
}
func addWi() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWio() {
for i := 0; i < 2000; i++ {
x += 1
}
}
5. WaitGroup
因为不知道子协程具体执行时间和结束时间,可以用WaitGroup计数器,开启协程+1,执行借书-1,主协程堵塞直到计数器为零。
上面Goroutine中的代码可优化为下面代码,用var wg sync.WaitGroup
创建计数器, wg.Add(5)
往计数器输入子协程个数, defer wg.Done()
在子协程执行完后执行, wg.Wait()
堵塞主协程。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
println("hello goroutine:"+fmt.Sprint(i))
defer wg.Done()
}
func main() {
wg.Add(5)
for i:=0;i<5;i++ {
go hello(i)
}
wg.Wait()
}
二. 依赖管理
1. 背景
对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。
2. GO 依赖管理演进
而Go的依赖管理主要经历了3个阶段,分别是GOPATH,GO Vender,go module
到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标:不同环境 (项目) 依赖的版本不同,控制依赖库的版本
3. GOPATH
GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。
目录有以下结构:src
:存放Go项目的源码;pkg
:存放编译的中间产物,加快编译速度;bin
:存放Go项目编译生成的二进制文件。
GOPATH直接依赖src下的代码,通过go get
下载最新版本的宝到src目录下
4. GOPATH – 弊端
如图,同一个pkg,有2个版本,A->func A(),B->func B(),而src下只能有一个版本存在,那AB项目无法保证都能编译通过。也就是在gopath管理下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender出现了。
5. GO Vendor
Vendor 是当前创建的项目中的一个目录
,其中存放了当前项目依赖的副本
。在Vendor机制下,如果当前项目存在Vendor目录
,会优先使用该目录下的依赖,如果依赖不存在
,会从GOPATH中寻找;这样通过每个项目引入一份依赖的副本解决了多个项目需要同一个 package依赖的冲突问题。但vendor无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题。
6. GO Vendor – 弊端
如图项目A依赖pkg b和c,而B和C依赖了D的不同版本,通过vendor的管理模式我们不能很好的控制对于D的依赖版本,一旦更新项目,有可能带来无法控制依赖的版本
,更新项目又可能出现依赖冲突,导致编译出错
。归根到底vendor不能很清晰的标识依赖的版本概念。下面,go module就应运而生了。
7. Go Module
Go Module 是Go语言官方推出的依赖管理系统类似于java 的maven
,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题,Go 1.16 默认开启;我们一般都读为go mod,我们也先统一下名称,通过 go.mod 文件管理依赖包版本,通过 go get/go mod 指令工具管一下赖包
。
Go Module可以定义版本规则和管理项目依赖关系.
8. 依赖管理三要素
1.配置文件,描述依赖 go.mod
2.中心仓库管理依赖库 Proxy
3.本地工具 go get/mod
9. go.mod
首先模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是github前缀则表示可以从Github 仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go。mod文件进行管理。下面是依赖的原生sdk版本
最下面是单元依赖,每个依赖单元用模块路径+版本
来唯一标示。
indirect 表示间接依赖
主版本2+
模块会在模块路径增加/vN
后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。对于没有go.mod文件
并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀。
go.mod选择依赖时选择最低的兼容版本。
10. version
gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了方便管理则定义了版本规则, 分为语义化版本和基于commit伪版本;其中语义化版本包括${MAJOR}.${MINOR}.${PATCH}
,不同的MAJOR
版本表示是不兼容的 API,所以即使是同一个库,MAJOR
版本不同也会被认为是不同的模块;MINOR
版本通常是新增函数或功能,向后兼容;而patch
版本一般是修复 bug ;而基于commit的为版本包括vX.0.0-yyyymmddhhmmss-abcdefabcdef
,基础版本前缀是和语义化版本一样的;时间戳 (yyyymmddhhmmss)
, 也就是提交 Commit
的时间,最后是校验码 (abcdefabcdef)
, 包含 12 位的哈希前缀;每次提交commit后 Go 都会默认生成一个伪版本号。
11. 依赖分发-回源
gomodule的依赖分发。也就是从哪里下载,如何下载的问题
github是比较常见给的代码托管系统平台,而Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
但直接使用版本管理仓库下载依赖
,存在多个问题,首先无法保证构建确定性
:软件作者可以直接代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用;大幅增加第三方代码托管平台压力。
12. 依赖分发 – proxy
而go proxy就是解决这些问题的方案,Go Proxy 是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供“immutability
”和“available
”的依赖分发;使用 Go Proxy 之后,构建时会直接从Go Proxy
站点拉取依赖。类比项目中,下游无法满足我们上游的需求,
13. 依赖分发 – GOproxy
Go Modules通过GOPROXY环境变量控制如何使用 Go Proxy;GOPROXY是一个 Go Proxy 站点URL列表,可以使用“direct”表示源站。对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。