依赖管理和工程实践|青训营笔记

63 阅读6分钟

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

前言

这是跟随青训营学习的第二天,写文章的目的就是将今天学到的知识融会贯通,复习提升。也希望通过这样的方式与青训营的诸位一起进步!

并发编程

为什么go语言的速度为什么这么快?

image.png

协程goroutine

image.png

线程和协程的通俗说明:

通俗易懂的讲,线程是操作系统的资源,以Java为例,当java程序创建一个线程,虚拟机会向操作系统请求创建一个线程,虚拟机本身没有能力创建线程。而线程又是昂贵的系统资源,创建、切换、停止等线程属性都是重量级的系统操作,非常消耗资源,所以在java程序中每创建一个线程都需要经过深思熟虑的思考,否则很容易把系统资源消耗殆尽。
而协程,看起来和线程差不多,但创建一个协程却不用调用操作系统的功能,编程语言自身就能完成这项操作,所以协程也被称作用户态线程。我们知道无论是java还是go程序,都拥有一个主线程,这个线程不用显示编码创建,程序启动时默认就会创建。协程是可以跑在这种线程上的,你可以创建多个协程,这些协程跑在主线程上,它们和线程的关系是一对多。如果你要创建一个线程,那么你必须进行操作系统调用,创建的线程和主线程是同一种东西。显然,协程比线程要轻量的多。
与其他大部分语言提供的协程支持相同,Go 的 Goroutine 是用户态的,其协程栈占用仅有 KB 级别,十分节约系统资源;但不同的是,Goroutine 将协程和并发简化到了仅需一个 go 关键字即可完成,而不像其他语言的协程一样及其繁琐复杂。示例如下:


import (
	"fmt"
	"time"
)

func HelloPrint(i int) {
	 println("Hello goroutine : " + fmt.Sprint(i))
	
}

// 效果就是快速且无序打印

func HelloGoroutine() {
	for i := 0; i < 5; i++ {
		
		go func(j int) {
			HelloPrint(j)
		}(i)
	}
	// time.Sleep()的作用是:保证了子协程在执行完之前,主协程不退出。
	time.Sleep(time.Second)
}

func main() {
	HelloGoroutine()
}

通道Channel

go语言提倡通过通信共享内存而不是通过内存共享通信!
通道是用来传递数据的一个数据结构,通过传递指定类型的值来同步通讯
操作符<-用于指定通道的方向,实现发送or接收

image.png
通道定义:

make(chan 元素类型,[缓冲大小])
示例如下:


import (
	"fmt"
)

func CalcPow() {
	src := make(chan int)
	dest := make(chan int, 3)
	// 子协程src发送0~9数字
	go func() {
		defer close(src) // 当子协程src结束的时候再关闭,减少资源浪费
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	// 子协程dest计算输入数字的平方
	go func() {
		defer close(dest)
        // 通过 range 关键字来实现遍历读取到的数据
		for i := range src {
			dest <- (i * i)
		}
	}()
	// 主协程输出最后的答案
	// 这里可以暂时认为子协程需要使用匿名函数
	for i := range dest {
        // 因为主协程可能会有更多的复杂操作,比较耗时,所以用带缓冲的通道可以避免问题
		fmt.Println(i)
	}
}

func main() {
	CalcPow()
}

并发安全问题与LOCK

我们先看一个简单的程序:

​
func addWithoutLock() {
    for i := 0; i < 2000 ; i++ {
        x += 1
    }
}
​
func main(){
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock:",x)
}

程序很简单,就是5个协程并发执行,每个协程计数到2000,理论上结果应该是10000,但最终结果会比10000小,为什么?
当多个线程同时执行,多个线程之间是相互抢占资源执行,并且抢占是发生在线程的执行的每一步过程中,导致出现非法数据。这种现象就称之为多线程的并发安全问题
如何解决呢?我们需要一把“锁”,引入并发锁 sync.Mutex。当有协程操作资源时,将该资源锁住,不让其他协程使用。例如:

    x    int64
    lock sync.Mutex
)
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
​
func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)
}

并发锁的使用要十分谨慎,请尽在有并发安全的代码中使用并发锁,因为并发锁的使用实际上将并行的程序串行化,会导致显著降低性能;同时,不当的锁使用也可能导致死锁(DeadLock)等问题发生,即两个协程互相锁住对方需要之后操作的资源,导致代码卡死。

WaitGroup实现同步


import (
	"fmt"
	"sync"
)

func HelloPrint(i int) {
	fmt.Println("Hello WaitGroup :", i)
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5) //Add方法:计数器加5
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done() //计数器—1
			HelloPrint(j)
		}(i)
	}
	wg.Wait()//阻塞直到计数器为0
}

func main() {
	ManyGoWait()
}

依赖管理

最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,应该怎么解决?

go依赖管理的演进

GOPATH

是go语言支持的环境变量,有如下三个部分:

  • bin:项目编译的二进制文件
  • pkg:项目编译的中间产物,加速编译
  • src:项目源码,项目代码直接依赖src下的代码go get下载最新版本的包到src目录下
    缺点:无法实现package的多版本控制。

Go Vendor

通过在项目目录下新建 vendor 文件夹,并存放依赖库文件副本的方式,使得不同项目可以依赖不同的依赖库版本,解决了版本冲突的问题。
但是,他更新项目的时候可能导致编译错误的冲突且无法控制依赖的版本。

Go Module

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

依赖管理的三要素

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

go mod

image.png

version

version的两种类型:

  • 语义化版本
  • 基于commit版本

image.png

indirect

表示该模块没有直接依赖

incompatible

表示要按照不同的模块来处理相同项目不同主版本的依赖

依赖分发,回源

即依赖从哪里下载,如何下载的问题

image.png

变量 GOPROXY

整体的依赖寻址路径,会先从proxy1开始,若没有,去proxy2,若无,去原站

image.png

工具 go get

image.png

工具 go mod

image.png