Go语言进阶与依赖管理 |青训营

89 阅读3分钟

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:增加需要的依赖,删除不需要的依赖