Go语言进阶 | 青训营笔记

72 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

并发&&并行

优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色

  • 多线程程序在单核心的 cpu 上运行,称为并发。
  • 多线程程序在多核心的 cpu 上运行,称为并行。

并发
并发主要是指通过切换时间片来实现的伪同时运行,可以达到压榨cpu性能的目的,但是上下文切换是有成本的,不仅占内存,而且多个线程共享同一个资源时可能会发生死锁。

并行
并行和并发是有可能同时发生的,即混用,且并发和并行是操作系统根据cpu的实际占用情况决定的,并非人为决定,即一个宏观上的并发有可能是并发,也有可能是并行。而无论是并发还是并行都有利于都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。

协程Coroutine

协程并不是Go语言特有的机制,像Lua、Ruby、Python、Kotlin、C/C++等也都有协程的支持,区别在于有的是从语言层面支持、有的通过插件类库支持,而Go语言是原生语言层面支持

协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈,Go语言中,用关键字go开启多协程工作,代码如下:

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)  //等待组的计数器 +1
      go func(j int) {
         defer wg.Done()  //等待组的计数器 -1
         hello(j)
      }(i)  //函数匿名调用
   }
   wg.Wait()  //当等待组计数器不等于 0 时阻塞直到变 0
}

通道Channel

image.png

在Go语言中channel作为通道负责协程之间的通信,通过make创建两个通道,分别视作生产者和消费者,一个负责发出数据,一个负责接收数据:
func CalSquare() {
   src := make(chan int)  //创建通道src
   dest := make(chan int, 3)  //创建缓冲区容量为3的通道dest
   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

使用lock对协程加锁:

//创建lock,sync.Mutex指定锁类型为互斥锁
var lock sync.Mutex
//方法同java中的lock一致
lock.Lock()
lock.Unlock()

对于互斥锁应该注意,如果一个协程已经拿到了锁,再次加锁时该锁已被占用,协程会自动挂起等待别的协程释放锁,但此锁还没有被该协程释放,因此会出现死锁的情况。

依赖管理

依赖管理为了解决依赖冲突应运而生

GOPATH

一开始,go直接使用GOPATH下的src存放依赖源码,这样所有项目的依赖都指向GOPATH,由于go没用版本管理的手段,导致不同项目对于某个依赖的版本不同产生冲突。

Go Vendor

后看,go通过引入Go Vendor,在项目目录下新建vendor文件夹用于存放依赖库文件副本,使得不同项目可以依赖不同的依赖库版本,但是,当发生次级依赖,也就是某个项目所依赖的多个项目依赖于某一个其他项目,此时依旧产生依赖冲突。

Go Module

最后,go采用了Go Module进行依赖管理并沿用至今,Go Module通过项目路径中的go.mod文件声明所需依赖的名称和版本范围,并通过go.sum文件记录项目实际使用的依赖和版本,在go中我们可以通过使用go.get和go.mod两个工具进行依赖的添加和移除。

通过本次课程的学习,我们了解到了go语言在处理问题时有着独到而又先进的思想,包括用户态的栈kb级的协程,以及类似目前主流却又更加方便地依赖管理