Go依赖管理 | 青训营笔记

132 阅读8分钟

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

一、本堂课重点内容:

  • 依赖管理的背景
  • 依赖管理的演进
  • 依赖分发

二、详细知识点介绍:

背景知识

在我们日常开发项目过程中,我们基本上不会所有的代码都是只根据标准库来进行构建,我们会去导入大大小小的包或者引入许多依赖,从而来 -偷懒- 避免重复“造轮子”

那么随着项目体量的不断变大,其所依赖的大大小小的库也不断变多,如何管理这些依赖就成了大问题

依赖管理的演进

image.png GOPATH

首先,path这个单词搞编程的肯定都不陌生,GOPATH就是Go语言支持的一个环境变量,它的value值就是Go项目的工作区,其中这个工作区的目录有以下结构:

  • bin:主要用来存储项目编译后的二进制文件
  • pkg:用来存放项目编译的中间产物,可以加速编译的速度
  • src:主要是存放项目源码的

项目的依赖主要是通过 go get命令(go1.17被弃用)下载最新版本的包到src目录下,项目的源码就全都依赖src目录里的代码包。

但是这样子是有一个很大的弊端的: 如果项目中有两个子工程依赖同一个包的不同版本的话,在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这种项目依赖需求是不能被满足的。

为了解决这一问题,产生了一种新的管理模式——Go Vendor

image.png

Go Vendor

在先前的GOPATH管理模式下,面临的最大问题就是无法实现package的多版本控制问题

我们来想一想,这个是如何解决呢?

有一个非常简单粗暴的解决方案呼之欲出——我们只要把不同package版本的依赖包都保存下来不就好了

是的,Go Vendor就是这样解决的,Vendor 是当前项目中的一个目录,其中存放了当前项目依赖的副本。在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找;这样通过每一个项目都引入一份依赖的副本,这样就解决掉了多个项目需要同一个package依赖冲突问题。

但是,Go Vendor同样的也有着它的弊端:

在Go Vendor中,它无法控制所依赖的版本,而且在更新项目之后可能会导致之前的版本与更新后的版本不兼容的问题,从而出现依赖冲突,导致编译出错。

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

image.png

Go Module

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

通过go module进行依赖管理的核心目标就在于定义版本规则和管理项目的依赖关系

主要通过以下两个方法来实现该目的:

  • 通过go.mod文件管理依赖包版本
  • 通过go get(1.17已被弃用)/go mod指令工具来管理依赖包

进行依赖管理的三要素:

  • 配置文件,描述依赖 (go.mod)
  • 中心仓库管理依赖库 (Proxy)
  • 本地工具 (go get/mod)
module example/project/app  // 依赖管理的基本单元

go 1.19 // 原生库

require( // 单元依赖
    example/lib1 v1.0.2
    example/lib2 v1.0.0 // indirect
    example/lib3 v0.1.0-20191116025543-5a5fe074e612
    example/lib4 v0.0.0-20180319013603-bacd8c6ef2ee // indirect
    example/lib5/v3 v3.0.2
    example/lib6 v3.2.0+incompatible
)

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

当然,这个版本号也是有讲究的,主要有两种定义版号的版本:

image.png

gopath和govendor都是源码副本方式依赖,没有版本规则概念

而gomod为了方便管理则定义了版本规则, 分为语义化版本和基于commit的伪版本;

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

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

module example/project/app  // 依赖管理的基本单元

go 1.19 // 原生库

require( // 单元依赖
    example/lib1 v1.0.2
    example/lib2 v1.0.0 // indirect
    example/lib3 v0.1.0-20191116025543-5a5fe074e612
    example/lib4 v0.0.0-20180319013603-bacd8c6ef2ee // indirect
    example/lib5/v3 v3.0.2
    example/lib6 v3.2.0+incompatible
)

依赖单元中的特殊标识符:

首先是indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,表示间接依赖

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

依赖分发

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

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

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

首先无法保证构建确定性,软件作者可以直接代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。

其次无法保证依赖可用性,依赖软件作者可以直接代码平台删除软件,导致依赖不可用;大幅增加第三方代码托管平台 压力。

image.png

而go proxy就是解决这些问题的方案,Go Proxy 是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供“immutability”和“available”的依赖分发;使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖。

image.png

下面讲一下go proxy的使用

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

image.png

最后介绍一下go get 和 go mod工具的使用吧

image.png

image.png

三、课后个人总结:

ok!这次通过对Go依赖管理的学习,令我收获良多

在学习开发刚开始的时候,我对于依赖管理的意识是比较淡薄的,因为感觉这个东西不就是“导包”嘛~ 哪有那么多花里胡哨的?

直到深入了解分析了各种案例之后,也越发觉得依赖管理也是开发项目中非常重要的一个部分,除非你要从0开始敲代码。

一个更加合适且科学的依赖管理方式对于一个项目的开发效率提升来说,是非常有必要的,这次的学习不仅让我知道了关于Go的依赖管理方式和演变,也让我知道了在项目中依赖管理的一些痛点是在哪,这个是适用在每一个项目中的,而不仅仅是Go。

总之,收获满满呀!