go协程与依赖管理 | 青训营笔记

99 阅读2分钟

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

课堂笔记与思考

并发编程

并发

并发就是多个任务在一个核心的cpu上执行,cpu在一个最小时间段内只能执行一个任务,但是可以通过快速的切换任务来达到类似任务同时进行的效果,因为计算机切换任务的速度远远大于人的反应速度,所有在人看来任务就是同时进行的。

并行

并行就是在多个核心的cpu上执行任务,那么每个核心都分配一个任务执行就能够达到多个任务同时执行的效果。go存在一种调度模型,这种调度模型可以极大的提高并发任务的执行效率。

goroutine(协程)

线程(MB级别)属于内核态,调度他的话会更消耗系统资源。于是go中存在一种叫协程(KB级别)的东西,协程的创建和调度不需要系统执行,它的创建和调度由go本身完成。协程就像是一种类似与线程的更小的单位它可以在线程中并发的执行。go语言甚至可以一次创建上万个协程。这也是go更适合高并发场景的原因所在。

使用go关键字开启协程

 package main
 ​
 import (
    "fmt"
    "time"
 )
 ​
 func main() {
    helloGoRoutine()
 }
 ​
 // 在协程中执行的函数
 func print(i int) {
    //执行打印
    //fmt.Sprint(i)将数字i返回成字符串格式
    println("hello goroutine : " + fmt.Sprint(i))
    /*
       println和fmt.println的区别:
       1.是否需要导入fmt包
       2.println是标准错误输出,也就说说它的输出结果字体颜色是红色,一般用与debug调试,并且官方说明未来不一定会存在;
       而fmt.println是标准输出,字体为白色
       3.fmt.println可以实现println无法实现的字节数统计和错误分析
       4.println参数不能接收数组和结构体
       5.对于组合类型的参数,println函数将输出参数的底层地址,而fmt打印函数将输出接口参数的动态值
       6.如果一个实参有String() string或Error() string方法,那么fmt打印函数在打印参数时会调用这两个方法;
       而println函数则会忽略参数的这些方法。
    */
 }
 ​
 func helloGoRoutine() {
    for i := 0; i < 5; i++ {
       //go关键字开启一个协程,此处使用匿名函数直接执行
       go func(j int) {
          print(j)
       }(i)
    }
    //保证子协程执行完成之前,主线程不退出
    time.Sleep(time.Second)
 }

CSP

go提倡通过通信来实现共享内存,不提倡通过共享内存来实现通信。

Channel(通道)

通道是一种用来作为协程之间通信的一种数据结构,它分为有缓冲通道和无缓冲通道(又名同步通道,因为通道没有缓冲,协程之间直接收到对方发送是数据)

 make(chan int, 2)
 //make(chan 数据类型,[缓冲区大小])

通信共享内存

协程之间生产者消费者模式通信(通过通信共享内存,不需要加锁)

 func main() {
    calSquare()
 }
 ​
 // 协程通信
 func calSquare() {
    src := make(chan int)     //无缓冲通道
    dest := make(chan int, 3) //有缓冲通道
    //开启一个协程A将数字放入无缓冲通道
    go func() {
       defer close(src) //延迟关闭
       for i := 0; i < 10; i++ {
          src <- i
       }
    }()
    //开启一个协程B将无缓冲通道的数字接收处理后放入有缓冲通道
    go func() {
       defer close(dest)    //延迟关闭
       for i := range src { //rang循环遍历通道
          dest <- i * i
       }
    }()
 ​
    for i := range dest { //rang循环遍历,取出通道内的数据
       println(i)
    }
 }

共享内存通信

 func main() {
    add()
 }
 ​
 // 定义变量
 var (
    x    int
    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() {
     //1
    x = 0
    for i := 0; i < 5; i++ {
       go addWithoutLock()
    }
    time.Sleep(time.Second) //睡眠等待所有协程执行完毕
    println("addWithoutLock : ", x)
     //2
    x = 0
    for i := 0; i < 5; i++ {
       go addWithLock()
    }
    time.Sleep(time.Second)
    println("addWithLock : ", x)
 }

waitGroup

在上面的生产者消费者模式中,使用time.sleep等待协程执行完毕其实是不好的。更好的一种方式是使用waitGroup

 func main() {
    manyGoWait()
 }
 ​
 func manyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5) //告诉waitgroup需要开启5个协程
    for i := 0; i < 5; i++ {
       //开启协程
       go func(j int) {
          defer wg.Done() //当协程结束,告诉waitgroup减少一个运行的协程数
          print(j)
       }(i)
    }
    //等待waitgroup中统计的协程数为0
    wg.Wait()
 }

依赖管理(go module)

  • 通过go.mod文件管理依赖包的版本
  • 通过go get/go mod指令工具管理依赖包

依赖管理三要素

  1. 配置文件,描述依赖——go.mod
  2. 中心仓库管理依赖库——Proxy
  3. 本地工具——go get/mod

版本管理

版本规则

语义版本

MAJOR.{MAJOR}.{MINOR}${PATCH}

MAJOR:标识了一个大版本,大版本之间可以互不兼容

MINOR:新增功能,但是需要在大版本下保持各个版本之间的兼容

PATCH:代码修复

基于commit的伪版本

vX.0.0-yyyymmddhhmmss-abcdefgh1234

X:版本前缀,和语义版本一致

yyyy...:提交某次commit的时间戳

abc:提交commit的哈希码前12位

关键字

indirect

项目依赖中有直接依赖和间接依赖,间接依赖的依赖会被标识为indirect

incompatible

对于主版本号v2及以上版本的依赖,为了兼容以前一些老仓库没有使用go module的依赖加上关键字incompatible标识可能存在不兼容问题

依赖分发——Proxy

goProxy会缓存第三方代码平台的依赖,保证我们的项目构建的稳定性和可靠性。我们可以直接从goProxy拉取依赖

设置goProxy

GOPROXY="xxx1.cn, xxx2.cn, direct"

proxy链依赖,首先会从1开始拉取依赖,拉取不到找2,最后会从代码原站拉取。direct就代表代码原站

工具——go get(拉取依赖)

image-20230204162248943.png

go mod

image-20230204162426152.png