Go语言并发编程及依赖管理

💟这里是CS大白话专场,让枯燥的学习变得有趣!
💟没有对象不要怕,我们new一个出来,每天对ta说不尽情话!
💟好记性不如烂键盘,自己总结不如收藏别人!

🧡并发编程

Goroutine

💌 为什么 Go 语言运行效率如此之高呢?因为它可以充分发挥多核优势,这里有个重要的概念:Goroutine(协程),Go 语言是利用协程达到高并发效果的。

image.png

  • 进程:程序一次动态执行的过程,操作系统资源分配最小单位,有独立内存空间。
  • 线程:轻量级进程,CPU调度的最小单位,多个线程共享所属进程的资源,MB级别。
  • 协程:轻量级线程,由用户控制调度,KB级别。

💌 如何开启一个协程呢?我们只需要在调用的函数前面添加 go 关键字,就能使这个函数以协程的方式运行。

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) // go + 调用函数名(参数)
   }
   time.Sleep(time.Second) // 阻塞,保证子协程执行完之前主线程不退出
}

可以看到打印结果是并发乱序输出的:

image.png

CSP(Communicating Sequential Processes)

💌 Go 语言中协程之间的通信提倡通过通信共享内存,而非通过共享内存实现通信,二者都支持实现,但是听起来很容易混淆,通过画图可以直观演示:

image.png

🍠 通过通信共享内存:通过通道给目标协程发送信息。Channel(通道)分为无缓冲通道和有缓冲通道,构造方式:

  • 无缓冲通道:make(chan int)
  • 有缓冲通道:make(chan int,2)

image.png

如下代码所示,构造生产者A和消费者B可以实现有缓冲通道的协程通信,生产者产生数字,消费者计算数字的平方,并顺序打印。

package main

func CalSquare() {
   src := make(chan int)     // 生产者
   dest := make(chan int, 3) // 消费者,带缓冲区
   go func() {               // A协程发送0~9至src中
      defer close(src) // defer表示延迟到函数结束时执行,用于释放已分配的资源
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()
   go func() { // B协程计算src中数据的平方,并发送至dest中
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()
   for i := range dest {
      println(i)
   }
}

func main() {
   CalSquare()
}

🍠 通过共享内存通信:需要通过 sync 包中的 lock 进行加锁,如果不加锁会出现问题。如下代码开启5个协程,每个协程进行2000次+1操作,正确结果应该是10000,不加锁的结果不准确。项目中应避免对共享内存进行非并发安全的读写操作。

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)
   x = 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   println("WithLock x =", x)
}

func main() {
   Add()
}

image.png

💌 sync 包中的 WaitGruop 也可以实现并发任务同步,Add(delta int) 开启 delta 个协程,Done() 结束协程,Wait() 阻塞主线程。

image.png

🧡依赖管理

依赖演变

💌 Go 依赖主要经历了 GOPATH -> Go Vendor -> Go Module 演变,现在主要采用 Go Module 方式。不同环境(项目)依赖的版本不同,需要控制依赖库的版本。

  • GOPATH:环境变量,Go 语言的工作区,通过 go get 下载最新版本的包到 src 目录下,项目代码直接依赖 src 下的所有源代码。但是有一个弊端就是无法实现package的多版本控制,A、B 项目可能依赖同一个包的不同版本。
  • Go Vendor:在项目目录下新增 vendor 文件夹,所有依赖包副本形式放在其中,通过 vendor => GOPATH 的方式寻址。但是还有问题就是更新项目时可能出现依赖冲突,依然无法控制依赖的版本。
  • Go Module:通过 go.mod 文件管理依赖包版本,通过 go get/go mod 指令工具管理依赖包。既能定义版本规则,又能管理项目依赖关系。

依赖管理三要素

🍠 配置依赖 go.mode

module example/project/app //依赖管理基本单元

go 1.16  //原生库版本

require (  
    //单元依赖标识语法:[Module Path][Version/Pseudo-version]
    example/lib1 v1.0.2
    example/lib2 v1.0.0 // indirect
    example/lib3 v0.1.0-20190725025543-5a5fe074e612
    example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect
    example/lib5/v3 v3.0.2  // 主版本2+的模块会在路径后增加/vN后缀
    example/lib6 v3.2.0+incompatible  // 对于没有go.mod文件且主版本2+的依赖+incompatible
)

go.mode 定义了自己的版本规则,分为语义版本和基于commit伪版本。

  • 语义版本:${MAJOR}.${MINOR}.${PATCH}

    v版本.新增功能.修复bug,不同版本不兼容,功能向下兼容。

  • 基于commit伪版本:${vx.0.0-yyyymmddhhmmss-abcdefgh1234}

    v版本-时间戳-校验码。

🍠 中心仓库管理依赖库 Proxy

对于 go.mod 中定义的依赖,可以从对应仓库(Github)中下载指定软件依赖,从而完成依赖分发,但是会产生如下问题:

  • 无法保证构建确定性:软件作者直接修改软件版本,导致下次构建使用其他版本的依赖,或者找不到依赖版本。
  • 无法保证依赖可用性:软件作者删除软件,导致依赖不可用。
  • 增加第三方压力:代码托管平台负载过大。

可以使用 Go Proxy 解决上述问题,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,构建时会直接从 Go Proxy 站点拉取依赖。Go Modules通过 GOPROXY 环境变量控制依赖寻址顺序:如 proxy1 -> proxy2 -> 源站

GOPROXY="https://proxy1.cn, https://proxy2.cn,direct"

🍠 本地工具 go get/mod

image.png
image.png

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值