Go语言进阶与依赖管理 | 青训营

63 阅读8分钟

语言进阶——并发编程

并发VS并行

  1. 并行:多线程程序在一个核的CPU上运行

  2. 多新城程序在多个核的CPU上运行

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

Goroutine(协程)

协程:用户态,轻量级编程,栈KB级别

线程:内核态,线程跑多个协程,栈MB级别

示例

快速打印hello goroutine:0→hello goroutine:4

快速意味着需要开多个协程去打印

只需要在调用函数前面加上go关键字,就可以为该函数调用协程运行

time.Sleep(time.Second)主要是为了保证在子协程完成之前,主线程退出。

CSP(Communicating Sequential Process)

提倡通过通信共享内存而不是通过共享内存实现通信

Channel

一种引用类型,创建需要通过make关键字

make(chan 元素类型,[缓冲大小])

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

示例

  • A 子协程发送0-9数字
  • B子协程计算输入数字的平方
  • 主协程输出最后的平方数

并发安全Lock

对变量执行2000次+1操作,5个协程并发执行

WaitGroup

计数器

开启协程+1;执行结束-1;主协程阻塞知道计数器为 0。

示例

让我们回到最初多个协程打印hello gorouting的例子,现在我们用watigroup实现协程的同步阻塞。

首先通过add方法,对计数器+5(因为要开启5个协程),然后开启协程,每个协程执行完后,通过done对计数器减少1,最后wait主协程阻塞,计数器为0 退出主协程。下边是最终的输出结果。

小结

整个章节主要涉及3个方面

  • 一个是协程Goroutine,通过高效的调度模型实现高并发操作
  • 一个是通道channel,通过通信实现共享内存
  • 最后sync相关关键字,实现并发安全操作和协程间的同步。

依赖管理

依赖指各种开发包,我们在开发项目中,需要学会站在巨人的肩膀上,也就是利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。

背景

对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要

Go依赖管理演进

Go的依赖管理主要经历了3个阶段,分别是,GOPATH→Go Vendor→到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标来迭代发展的,分别是

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本

GOPATH

GOPATH是Go语言支持的一个环境变量,是Go项目的工作区。

目录有以下结构:

  • src:存放Go项目的源码;
  • pkg:存放项目编译的中间产物,加快编译速度;
  • bin:存放Go项目编译生成的二进制文件

项目代码直接依赖src下的代码

go get下载最新版本的包到src目录下

弊端

同一个pkg,有2个版本,A->A(),B->B(),而src下只能有一个版本存在,那AB项目无法保证都能编译通过。也就是在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。

场景:A和B依赖于某一package的不同版本

问题:无法实现package的多版本控制

为了解决这问题,govender出现了。

Go Vendor

项目目录下增加Vendor 目录,其中存放了当前项目依赖的副本,所有依赖包副本形式放在$ProjectRoot/vendor

依赖寻址方式:vendor⇒GOPATH,在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找。

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。

但vendor无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题

弊端

如图项目A依赖pkg B和C,而B和C依赖了D的不同版本,通过vendor的管理模式我们不能很好的控制对于D的依赖版本,一旦更新项目,有可能带来。

问题

  • 无法控制依赖的版本
  • 更新项目又可能出现依赖冲突,导致编译错误

归根到底vendor不能很清晰的标识依赖的版本概念。下面,go module就应运而生了。

Go Module

Go Modules 是Go语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题,go module从Go 1.11 开始实验性引入,Go 1.16 默认开启;我们一般都读为go mod

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

终极目标:定义版本规则和管理项目依赖关系

依赖管理三要素

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

依赖配置

go.mod

依赖标识:[Module Path][Version/Pseudo-version]

  • 首先模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是github前缀则表示可以从Github 仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go。mod文件进行管理。
  • 下面是依赖的原生sdk版本
  • 最下面是单元依赖,每个依赖单元用模块路径+版本来唯一标示。

version

  • 语义化版本

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

    v1.3.0

  • 基于commit伪版本

    vX.0.0-yyyymmddhhmmss-abcdefgh1234

    v0.0.0-20220401081311-7dgdy3883de

gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了方便管理则定义了版本规则,分为语义化版本和基于commit伪版本;

其中语义化版本,不同的MAJOR 版本表示是不兼容的 API,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块;MINOR 版本通常是新增函数或功能,向后兼容;而patch 版本一般是修复 bug ;

而基于commit的伪版本,基础版本前缀是和语义化版本一样的;时间戳 (yyyymmddhhmmss), 也就是提交 Commit 的时间,最后是校验码 (abcdefabcdef), 包含 12 位的哈希前缀;每次提交commit后 Go 都会默认生成一个伪版本号。

indirect

下面我们再来看下依赖单元中的特殊标识符,首先是indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖,例如

A→B→C

  • A→B直接依赖
  • A→C间接依赖

incompatible

下一个常见是的是incompatible,主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。由于gomodule是1.11实验性引入,所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在v2或者以上的依赖,会在版本号后加上+incompatible 后缀

前面讲语义化版本提到,对于同一个库的不同的major版本,需要建立不同的pkg目录,用不同的gomod文件管理,如下面仓库为例,V1版本gomod在主目录下,而对于V2版本,则单独简历了V2目录,用另一个gomod文件管理依赖路径,来表明不同major的不兼容性。那对于有些V2+tag版本的依赖包并未遵循这一定义规则,就会打上incompatible标志

依赖图

如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目版本是?

  • v1.3
  • v1.4
  • A用到C时用v1.3编译,B用到C时用v1.4编译

选v1.4,应该选择最低的兼容版本

依赖分发

回源

下面讲一下gomodule的依赖分发。也就是从哪里下载,如何下载的问题

github是比较常见给的代码托管系统平台,而Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。

但直接使用版本管理仓库下载依赖,存在多个问题:

  • 首先无法保证构建确定性:软件作者可以直接在代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。
  • 无法保证依赖可用性:软件作者可以直接在代码平台删除软件,导致依赖不可用;
  • 大幅增加第三方代码托管平台压力:代码托管平台负载问题

Proxy

而go proxy就是解决上述问题的方案,Go Proxy 是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了“immutability”和“available”的依赖分发;使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖。类比项目中,下游无法满足我们上游的需求。

变量GOPROXY

下面讲一下go proxy的使用,Go Modules通过GOPROXY环境变量控制如何使用 Go Proxy;

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

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

工具

go get

go get example.org/pkg

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

go mod

go mod

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

尽量提交之前执行下go tidy,减少构建时无效依赖包的拉取