go语言进阶&依赖管理 | 青训营笔记

62 阅读3分钟

go语言进阶&依赖管理 | 青训营笔记

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

语言进阶

从并发编程的视角了解Go高性能的本质

Goroutine

Go可以充分发挥多核优势,高效运行

  • 协程:用户态,轻量级线程,栈MB级别
  • 线程:内核态,线程跑多个协程,栈KB级别

总的来说协程比线程轻量很多,而go语言会自己完成协程的创建和调度。

创建协程

在调用函数前面加上go关键字即可调用协程

快速打印helloworld示例:

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

func main() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

/********************
hello goroutine: 0
hello goroutine: 2
hello goroutine: 3
hello goroutine: 4
hello goroutine: 1
*********************/

可以看到,输出是乱序的,也就是并行输出。

CSP

提倡通过通信共享内存(通道channel)而非通过共享内存(临界区)实现通信。

后者需要mutex进行互斥和加锁,在一定程度上会影响程序的性能

Channel

通过通信实现共享内存

创建命令:make(chan 元素类型, [缓冲大小])

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

无缓冲通道又被称为同步通道,缓冲通道就相当于暂存区(缺氧里面的储气罐)

下面看一个简单的生产者-消费者模型。

func CallSquare() {
	src := make(chan int)
	dest := make(chan int, 3)
	go func() {
		//A子协程发送0-9
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	go func() {
		//B子协程计算输入数字的平方
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()

	for i := range dest {
		//主协程输出最后的平方数
		println(i)
	}
}

src和dest的传递能保证并发安全。

由于消费的速度可能比生产慢点,所以开了点缓冲。这样就不会因为消费者而影响生产者的执行效率。

也就是说,缓冲区的出现可以解决生产者消费者效率不匹配进而引发的问题。也很好理解,缺氧的自循环厕所中,芦苇就起到了缓冲区的作用。生产氧气的储气罐也是。

并发安全Lock

通过共享内存实现通信。没啥好讲的,要对共享的变量进行处理时一定要加锁,否则就会出现问题。之前写Springboot的时候已经碰到过了。

WaitGroup

WaitGroup是sync包下的一个东西,有这么些方法

  • Add(delta int):计数器加delta
  • Done():计数器减1
  • Wait():阻塞直到计数器为0

可以这么用:开启子协程+1,执行结束-1,主协程阻塞直到计数器为0

利用这个玩意就可以把之前的代码修改一下成这样

 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()
 }

小结

  • 协程。go语言能通过高效的调度模型实现高并发操作
  • 通道。协程间通过通信实现共享内存
  • WaitGroup。可以实现并发安全操作和协程间的同步

依赖管理

依赖指的就是各种开发包。

Go依赖管理演进为GOPATH -> Go Vendor -> Go Module。

现如今基本用Go Module管理依赖

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

依赖管理三要素:

就像maven一样。

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

配置文件

以下是一个例子

 module example/project/app  //依赖管理基本单元
 ​
 go 1.16 //原生库
 ​
 require (
     example/lib1 v1.0.2
     example/lib2 v1.0.0 //indirect
     example/lib3 v0.1.0-xxxxx-xxxx
     example/lib4 v0.0.0-xxxx-xxxx //indirect
     example/lib5/v3 v3.0.2
     example lib6/v3.2.0+incompatible
     
 )
 ​

version

  • 语义化版本,如V2.3.4,V3.4.5
  • 基于commit伪版本,vX.0.0-时间戳-哈希码前缀

indirect

假设A依赖了B,B依赖了C,那么A与B是直接依赖,A与C是间接依赖。

go对于A与C的这种情况都会在注释里打上indirect,表示没有直接导入该依赖模块的包。

incompatible

  • 主版本2+模块会在模块路径增加/vN后缀
  • 对于没有go.mod文件并且主版本2+的依赖,会+incompatible

没咋听懂,据说是为了兼容性。go会根据一定的算法,选择最低的兼容版本

中心仓库管理依赖库

首先介绍一个概念叫依赖分发。依赖分发就是从哪里下载如何下载的问题。

假设直接从各个代码托管平台下载,会出现一些问题。比如,无法保证构建稳定性(增加/修改/删除软件版本),无法保证依赖可用性(删除软件),另外还会增加第三行代码托管平台的压力。

为了解决这个问题,Proxy就诞生了。Go Proxy是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用。使用Go Proxy后,构建时会直接从Go Proxy站点拉去依赖。

GOPROXY

GOPROXY是一个Go Proxy站点URL列表,可以使用“direct”表示源站。对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,去proxy2;如果proxy2不存在则会回源到源站直接下载依赖,缓存到proxy站点中。

就像cdn一样。

本地工具

go get

go get example.org/pkg默认情况下会直接下载最新版本。一些参数:

  • @update, 默认
  • @none, 删除依赖
  • @v1.1.2, 特定的tag版本(语义)
  • @24asdf5, 特定的commit
  • @master, 分支最新的commit

go mod

  • init,初始化然后创建一个go.mod文件
  • download,下载模块到本地缓存
  • tidy,增加需要的依赖,删除不需要的依赖