作者:非妃是公主
专栏:《Golang》
博客主页:https://blog.csdn.net/myf_666
个性签:顺境不惰,逆境不馁,以心制境,万事可成。——曾国藩
文章目录
- 一、语言进阶
- 1. 并发和并行
- 2. 协程(Goroutine)与线程
- 3. CSP(Communicating Sequential Processes)
- 4. Channel
- 5. 并发安全 Lock
- 6. WaitGroup
- 二、依赖管理
- 1. GOPATH
- 2. Go Vendor
- 3. Go Module
- Ⅰ. 依赖表述
- Ⅱ. 依赖管理
- Ⅲ. 本地工具
- ① go get
- ② go mod
一、语言进阶
1. 并发和并行
Go语言是一种并发性能很好的语言,它可以多核(多核指:一个电脑包含多个CPU)优势,更加高效地运行。
2. 协程(Goroutine)与线程
Go语言的并发编程,主要就是通过协程(Goroutine)来实现的。协程和线程类似,但又并不同于线程,它比线程更加轻量级。
协程:用户态,轻量级线程,栈KB级别;
线程:内核态,线程跑多个协程,栈MB级别。
3. CSP(Communicating Sequential Processes)
协程间通信与进程通信类似,主要有2种方式:
- 通道
- 临界区
对于这两种方式,Go语言采用第一种——通道方式,进行协程间的信息传递。即,下图中左边的这种方式:
这样做的优点是,通过通信共享内存性能较高,不会像临界区一样,存在性能较低的问题。
4. Channel
缓冲通道:
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2)
它可以采用上面make的方式进行初始化,第一个参数指定类型,第二个参数指定缓冲通道的大小。
一个示例代码如下:
package concurrence
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)
}
}
代码中关于defer主要用来进行延迟调用(程序执行完毕后,即将退出时,执行defer,详细的关于defer的介绍,请查看博客:Golang中的一些关键字),然后相当于通过src这一公共区域进行内存通信。
这个demo程序类似于生产者、消费者进程,输出结果如下:
5. 并发安全 Lock
锁的作用主要是用来保证在操作变量时,只有一个协程在操作,这时候另一个协程想要操作的时候,需要等待锁解除才行。这样的操作,可以保证程序执行的正确性,防止程序出错。
下面来看一个demo,这个demo的作用是执行2000次+1操作,然后采用5个协程进行。函数的定义如下。
首先,是带有锁的函数定义,在操作x的时候,锁住了lock,如下:
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
不带锁的定义,如下:
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
5个协程,每个执行2000次+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)
}
运行程序如下:
这时,很有意思的是,并没有发生错误!本来想着由于没有加锁,这样多个进程在操作数据的时候就可能在同一个时钟周期,而造成数据丢失。
于是,我分析了一下原因,现在的CPU为16核,甚至更多,5个协程可能可以在不同的不同核上并行去跑,之间并没有影响。也就是说,会不会出错误,可能和机子的性能有关。我的机子是16核的,因此,我进行了如下尝试,修改协程数为17,果然,触发了错误,没有锁的协程产生了操作丢失。
6. WaitGroup
相当于一个计数器,那么它的作用是什么呢?记录当前正在执行的协程数,当正在执行的线程数不为 0 的时候,就需要进行一个等待。
demo程序如下:
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()
}
其中 wg 表示创建的一个WaitGroup,通过Add可以添加一个协程数,Add可以为负数;
然后启动协程,通过延迟调用,当协程完成后,我们执行Done函数,这会对写成书 -1 。
最后通过 Wait 函数,如果正在运行的协程不为0(存在正在运行的协程),那么就不结束主进程。
关于WaitGroup的定义,可以查看源码及注释进行了解,如下:
Done 和 Wait,如下:
同时,有了 wg.Wait()
,既可以省略了之前的 time.Sleep(time.Second)
了。
二、依赖管理
随着功能的增强,同时也需要更多的依赖库,这时候,依赖管理就显得很重要了。
Golang的依赖管理按照发展历史,可分为3种,GOPATH,Go Vendor,Go Module。如下图:
1. GOPATH
首先,值得说明的是,不同于Java、C++、python等,在Golang中,是不存在项目(project)这一概念的,在Gopath路径下就是Golang的工作目录,存在三个文件夹,作用分别如图中标注:
弊端:无法实现package的多版本控制。比如,不同的程序用到了同一个package,但是又是不同版本的,这时候,Gopath这种管理方式就无法处理了。如下:
2. Go Vendor
由于上面的弊端,诞生了Go Vendor,相当于在每个项目下,建立了一个Vendor文件夹,里面存放项目中用到的package,当程序寻找依赖报的时候,先去Vendor中去寻找,如果找不到,再去Gopath下去寻找,这就避免了上面提到的 package版本
问题。
但这种方法同样存在着较大的问题,如果一个项目(A)用到的两个包(B和C),这两个包又依赖了同一个包D,但是用到的这同一个包但是版本不同且不兼容的包D,同样回出现如下 无法控制版本
的问题(即项目内出现此问题):
3. Go Module
Go Module解决了上面相同包,不同版本的问题。
和Mave很类似,其中包含三要素:
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
Ⅰ. 依赖表述
通过上面这两种标识,我们就可以将不同版本的包区分开来。