并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。
并行: 当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。
并行可以理解为是实现并发的一个手段,Go 语言在GOMAXPROCS数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。Go 可以充分发挥多核优势,高效运行。
\newline
以下例子,快速打印hello goroutine:0~hello goroutine:4
package main
import (
"fmt"
"time"
)
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)
}
func main() {
HelloGoRoutine()
}
Go 实现了两种并发形式,第一种是多线程共享内存,其实就是Java或C++等语言中的多线程开发;另一种是Go语言特有的,也是Go语言推荐的CSP并发模型。
提倡通过通信共享内存而不是通过共享内存而实现通信。
\newline
如果说goroutine是Go语言程序的并发体的话,那么channels就是它们之间的通信机制。一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值消息。
每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
声明通道:var 通道变量 chan 通道类型
创建通道:make(chan 数据类型, [缓冲大小])
以下实例中,A子协程发送0~9数字,B协程计算输入数字的平方,主协程输出最后的平方数。
// package concurrence
package main
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)
}
}
func main() {
CalSquare()
}
\newline
并发编程的核心概念是同步通信,但是同步的方式却有多种。以下示例以互斥量 sync.Mutex 来实现同步通信,对变量执行了2000次+1操作,5个协程并发执行。
package main
import (
"sync"
"time"
)
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)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
}(i)
}
wg.Wait()
}
func main() {
Add()
ManyGoWait()
}
\newline
以下示例中,计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0。
回到前面多个协程打印hello goroutine的例子,现在用waitgroup实现协程的同步阻塞,对于这种要等待 N 个线程完成后再进行下一步的同步操作,可以使用 sync.WaitGroup 来等待一组事件。首先通过 Add() 方法,对计数器+5,用于增加等待事件的个数;然后开启协程,每个协程执行完后,调用 wg.Done() 表示完成一个事件,对计数器-1;最后 Wait() 主协程阻塞,等待全部的事件完成,计数器为0退出主协程。
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
}(i)
}
wg.Wait()
}
上述内容中,协程通过高效的调度模型实现高并发操作,通道channel通过通信实现共享内存,最后sync相关关键字实现并发安全操作和协程间的同步。
\newline
依赖指各种开发包,在开发项目中,需要学会站在巨人的肩膀上,利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。
如下图,对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要
\newline
而Go的依赖管理主要经历了3个阶段,分别是GOPATH,Go Vender,Go Module,到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标来迭代发展:
\newline
GOPATH 是 Go 语言支持的一个环境变量,value 是 Go 项目的工作区,项目代码直接依赖 src 下的代码,go get 下载最新版本的包到 src 目录下。目录有以下结构:
以下例子,A 和 B 依赖于某一package的不同版本,就会出现问题: 无法实现package的多版本控制。
同一个 pkg,有2个版本,A->A(),B->B(),而 src 下只能有一个版本存在,那 A,B 项目无法保证都能编译通过,也就是在 gopath 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender 出现了。
\newline
Vendor 是当前项目中的一个目录,其中存放了当前项目依赖的副本。在 Vendor 机制下,如果当前项目存在 Vendor 目录,会优先使用该目录下的依赖,如果依赖不存在,会从 GOPATH 中寻找。
以下示例中,项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor,依赖寻址方式:vendor => GOPATH,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。
但 vendor 无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题,以下示例中,项目 A 依赖 pkg B 和 C,而 B 和 C 依赖了 D 的不同版本,通过 vendo r的管理模式不能很好的控制对于 D 的依赖版本,一旦更新项目,有可能出现依赖冲突,导致编译出错。
归根到底vendor不能很清晰地标识依赖的版本概念,于是go module应运而生。
\newline
Go Modules 是 Go 语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法赖同一个库的多个版本等问题,go module 从 Go 1.11 开始实验性引入,Go 1.16 默认开启,一般都读为go mod。
\newline
\newline
模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是 github 前缀则表示可以从 Github 仓库找到该模块。依赖包的源代码由 github 托管,如果项目的子包想被单独引用,则需要通过单独的init go.mod 文件进行管理。以下示例中,go 1.16 为依赖的原生 sdk 版本,最下面 require 是单元依赖,每个依赖单元用模块路径+版本来唯一标示。依赖标识:[Module Path] [Version/Pseudo-version]
\newline
gopath和govendor都是源码副本方式依赖,没有版本规则概念,而go mod为了方便管理则定义了版本规则,分为语义化版本和基于commit伪版本。
${MAJOR}.${MINOR}.${PATCH}
。不同的 MAJOR 版本表示是不兼容的 API,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块;MINOR 版本通常是新增函数或功能,向后兼容;而patch 版本-般是修复 bug。example:V1.3.0
,V2.3.0
。vx.0.0-yyyymmddhhmmss-abcdefgh1234
。基础版本前缀是和语义化版本一样的:时间戳 yyyymmddhhmmss,也就是提交 Commit 的时间,最后是校验码 abcdefabcdef,包含 12 位的哈希前缀,每次提交 commit 后 Go 都会默认生成一个伪版本号。example:v0.0.0-20220401081311-c38fb59326b7
,V1.0.0-20201130134442-10cb98267c6c
。\newline
以下示例中,依赖单元中的特殊标识符——indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖。
\newline
主版本2+模块会在模块路径增加/VN后缀,这能让 go module 按照不同的模块来处理同一个项目不同主版本的依赖。由于 go module 是 1.11 实验性引入,所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖会在版本号后加上+incompatible 后缀。
\newline
如果 X 项目依赖了 A、B 两个项目,且 A、B 分别依赖了 C 项目的 v1.3、V1.4 两个版本,最终编译时所使用的 C 项目的版本为如下哪个选项? (B)
A. v1.3
B. V1.4
C.A用到C时用v1.3编译,B用到C时用v1.4编译
选择最低的兼容版本。
\newline
gomodule的依赖分发,也就是从哪里下载,如何下载的问题。
github是比较常见给的代码托管系统平台,而 Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
但直接使用版本管理仓库下载依赖,存在多个问题:
\newline
go proxy 可以解决上述的一些问题,Go Proxy 是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供 "immutability“ 和 “available” 的依赖分发;使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖。类比项目中,下游无法满足上游的需求,可以使用 Proxy 来解决。
\newline
Go Modules 通过 GOPROXY 环境变量控制如何使用 Go Proxy; GOPROXY="https://proxy1.cn, https://proxy2.cn, direct"
GOPROXY 是一个 GoProxy 站点 URL 列表,可以使用 “direct” 表示源站。对于示例配置,整体的依赖寻址路径,会优先从 proxy1 下载依赖,如果 proxy1 不存在,会到 proxy2 寻找,如果 proxy2 中不存在,则会回源到源站直接下载依赖,缓存到 prox 站点中。
\newline
以下介绍go module的管理工具: