这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
Go语言进阶与依赖管理
1. 语言进阶
从并发编程的视角了解Go高性能的本质
并发vs并行
- 并发:多线程程序在一个核的CPU上运行,宏观上看似A和B都在同时运行,但在某个时间点上仅有一个在运行
- 并行:多线程程序在多个核的CPU上运行,真正意义上的同时运行
1.1 Goroutine - 协程
-
协程:用户态,轻量级线程,栈KB级别,协程的创建和调度由Go语言本身完成
-
线程:内核态,线程跑多个协程,栈MB级别,线程的创建、切换、停止属于系统操作,比较消耗资源
-
样例 hello goroutine
func hello(i int) { println("hello goroutine :" + fmt.Sprint(i)) } func HelloGoRoutine() { for i := 0; i < 5; i++ { go func(j int) { hello(j) }(i) } time.Sleep(time.Second) // 保证主协程在子协程后面退出 }函数运行结果为:
hello goroutine :1
hello goroutine :4
hello goroutine :3
hello goroutine :2
hello goroutine :0
该运行结果有随机性
1.2 CSP(Communicating Sequential Processes)
协程之间的通信
提倡通过通信共享内存而不是通过共享内存而实现通信。提到通信就要引出另一个重要的概念——通道(channel)。
1.3 Channel
make(chan 元素类型,[缓冲大小])
- 无缓冲通道
make(chan int) - 有缓冲通道
make(chan int,2)2表示通道中能存放的元素个数 - 样例:设置两个子协程,一个发送0~9数字,另一个计算输入数字的平方,最后通过主协程输出平方数
函数运行结果: 0 1 4 9 16 25 36 49 64 81func CalSquare() { src := make(chan int) dest := make(chan int, 3) go func() { defer close(src) // defer作延迟的资源关闭 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) } }
1.4 并发安全Lock
- 通过样例(对变量执行2000次加一操作,5个协程并发执行)来理解什么是共享内存引发的非并发安全的读写操作
虽然上面提到了要提倡通过通信来共享内存,但在使用过程中难免会有上面例子中通过共享内存来通信的情况。这时就要用到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 = 0 for i := 0;i < 5; i++ { go addWithLick() } time.Sleep(time.Second) println("WithLock:",x) }sync.Mutex(互斥锁),它能确保在任意时刻都只有一个协程(goroutine)访问资源,而其他协程都在等待。所以上述示例的结果中,有锁的函数得到10000,而无锁的函数得到的是一个<=10000的随机数。
1.5 WaitGroup
在上述协程和锁的例子中,都用到了time.Sleep(time.Second),即通过等待一秒的方式确保子协程在主协程前面完成,这是一种暴力阻塞的解决方式,因为我们实际上并不知道子协程完成的具体时间。在Go中,sync.WaitGroup利用计数器原理实现并发安全。
| 方法名 | 功能 |
|---|---|
| (wg *WaitGroup) Add(delta int) | 计数器+delta |
| (wg *WaitGroup) Wait() | 阻塞直到计数器为0 |
| (wg *WaitGroup) Done() | 计数器-1 |
- 下面我们用
sync.WaitGroup来对样例 hello goroutine 进行优化上述优化中,通过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() }Add开启5个协程,然后在每个协程执行完成之后通过Done方法对计数器减一,表示该协程已经结束,最后通过Wait对计数器进行阻塞。
2. 依赖管理
了解Go依赖管理的演进路线,这里的依赖指的是各种开发包。对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。
2.1 Go依赖管理演进
GOPATH -> Go Vendor -> Go Module
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
2.1.1 GOPATH
- 环境变量$GOPATH,是Go项目的工作区,目录有以下几个结构
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码
- 项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
2.1.2 GOPATH的弊端
- 场景:项目A和项目B依赖于某一package的不同版本
- 问题:无法实现package的多版本控制
- 具体分析:对于同一个pkg,有两个版本,A->A(),B->(),而src下只能有一个版本存在,那么A项目和B项目无法保证都能编译通过。即在GOPATH管理模式下,如果多个项目依赖同一个库,那么该库应该是一个版本,这很显然不能满足我们的项目依赖需求。于是乎,Go Vendor出现了。
2.1.3 Go Vendor
- 项目目录下增加vendor文件夹,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor=>GOPATH
- 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个pkg依赖的冲突问题。
2.1.4 Go Vendor的弊端
- 问题:无法控制依赖的版本,更新项目有可能出现依赖冲突,导致编译出错
- 具体分析:如图项目A依赖pkg b和c,而B和C依赖了D的不同版本,通过vendor的管理模式我们不能很好的控制对于D的依赖版本,一旦更新项目,有可能出现依赖冲突导致编译错误。归根到底vendor不能很清晰的标识依赖的版本概念。于是乎,Go Module应运而生。
2.1.5 Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
- 终极目标:定义版本规则和管理项目依赖关系
2.2 依赖管理三要素
- 配置文件,描述依赖 —— go.mod
- 中心仓库管理依赖库 —— Proxy
- 本地工具 —— go get/mod
2.2.1 依赖配置 - go.mod
module github.com/Moonlight-Zhao/go-project-example
go 1.16
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.6 // indirect
)
首先模块路径module用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是github前缀则表示可以从github仓库找到该模块,依赖包的源代码由github管理,如果项目的子包想被单独引用,则需要单独的init go。go 1.16表示依赖的原生sdk版本,require里的是单元依赖,每个依赖单元通过模块路径+版本来唯一标识。
2.2.2 工具 - go get
go get example.org/pkg@update默认拉取最新版本,@update可省略
2.2.3 工具 - go mod
go mod init—— 初始化,创建go.mod文件go mod download—— 下载模块到本地缓存go mod tidy—— 增加需要的依赖,删除不需要的依赖- 流程:最简单的go mod实现就是在go文件中import引用包写好后在项目的根目录下输入
go mod init project_name然后再go mod tidy就可以了,可能有拉取超时的问题,就试试go env -w GOPROXY=https://goproxy.cn,direct,当然最好还是挂全局代理。
总结
在Go语言进阶与依赖管理中,主要有以下几点内容
- Goroutine —— 实现高并发
- Channel —— 实现协程间通信来共享内存
- Sync包中的Lock,WaitGroup —— 实现并发安全操作和协程同步
- go get和go mod的简单使用