- 依赖
依赖指各种开发包,我们在开发项目中,需要学会站在巨人的肩膀上,也就是利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。
对于helloworld以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要而Go的依赖管理主要经历了3个阶段
GOPATH => GO Vendor => GO Module
GOPATH到目前被广泛应用的GO Module,整个演进路线主要围绕实现两个目标来迭代发展的,分别是- 不同环境(项目)依赖的版本的不同
- 控制依赖库的版本
2.GOPATH
GOPATH是Go语言支持的一个环境变量,Value是Go项目的工作区目录有以下结构:
|-src //存放Go项自的源码
|-pkg //存放编译的中间产物,加快编译速度;
|-bin //存放Go项目编译生成的二进制文件
其中项目代码字节依赖src下的代码,使用go get下载最新版本的包到src\目录下。
弊端:
如图,同一个
pkg,有2个版本,A->AO,B->BO,而src下只能有一个版本存在,那AB项目无法保证都能编译通过。也就是在GoPath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,Go Vendor出现了。
- Go Vendor
Vendor是当前项目中的一个目录,其中存放了当前项目依赖的副本。在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找;这样解决了多个项目需要同一个pkg依赖的问题。
弊端:
vendor无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题,下面我们看一个场景。
如图项目A依赖pkg b和c,而B和C依赖了D的不同版本,通过vendor的管理模式我们不能很好的控制对于D的依赖版本,一旦更新项目,有可能带来编译错误等问题。归根到底vendor不能很清晰的标识依赖的版本概念。Go Module就应运而生了。
4. Go Modules
Go Modules 是Go语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题,Go Module从Go 1.11开始实验性引入,Go 1.16 默认开启;我们一般都读为go mod。
其通过go.mod文件,实现了依赖包版本的管理。通过go get/go mod指令工具管理依赖包。最终实现了定义版本规则和管理项目依赖关系。
依赖管理三要素:
-
配置文件,描述依赖 go.mod -
中心仓库管理依赖库 Proxy -
本地工具 go get/modmodule后面跟路径表示你能在哪找到这个模块,pkg需要被引用==在包所在的目录下建立go.mod,这个路径可以看出从哪里找到该模块,如果是github前缀则表示可以从Github仓库找到该模块。
依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go.mod文件进行管理。- 下面是依赖的原生
sdk版本,也就是原生库的版本号,不同的项目可能需要不同的原生库支持。 - 最下面是单元依赖,每个依赖单元用模块路径+版本来唯一标示。
依赖标识:
[Module PATH] [Module Version/Pseudo Version]GoPath和Go Vendor都是源码副本方式依赖,没有版本规则概念,而Go Mod为了放方便管理则定义了版本规则,分为语义化版本和基于commit伪版本。
-
语义化版本:
${MAJOR}.${MINOR}.${PATCH}
例:
V1.3.0 V2.3.0其中不同的MAJOR版本,表示的是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块;MINOR版本通常是新增函数或功能,向后兼容;而patch版本一般是修复bug; -
基于
commit伪版本:
vx.0.0-yyyymmddhhmmss-abcdefgh1234
例:
v0.0.0-20220401081311-c38fb59326b7 v1.0.0-20201130134442-10cb98267c6c
基础版本前缀是和语义化版本一样的;时间戳(yyyymmddhhmmss)也就是提交Commit的时间,最后是校验码(abcdefabcdef),包含12位的哈希前缀,每次提交commit后Go都会默认生成一个伪版本号。
下面我们再来看下依赖单元中的特殊标识符,首先是indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖。
下一个常见是的是incompatible,主版本2+模块会在模块路径增加/vN后缀(例如:v1、v2),这能让Go Module按照不同的模块来处理同一个项目不同主版本的依赖。由于Go Module是Go 1.11实验性引入的,所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上incompatible后缀
前面讲语义化版本提到,对于同一个库的不同的major版本,需要建立不同的pkg目录,用不同的Go.Mod文件管理,如下面仓库为例,V1版本Go.Mod在主目录下,而对于V2版本,则单独建立了V2目录,用另一个Go.Mod文件管理依赖路径,来表明不同major的不兼容性。那对于有些V2+tag版本的依赖包并未遵循这一定义规则,增加一个incompatible的case。
如果X项目依赖了 A、B 两个项目,且A、B分别依赖了 C项目的v1.3、V1.4两个版本,最终编译时,会使用C 1.4的版本,这是由于Go在编译时会选择最低的兼容版本。
-
Go Module的依赖分发
Github是比较常见给的代码托管系统平台,而Go Modules系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。弊端:
- 无法保证构建确定性: 软件作者可以直接代码平台增加/修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。
- 无法保证依赖可用性: 依赖软件作著可以直接代码平合删除软件,导致依赖不可用:
- 大幅增加第三方代码托管平合压力。
Go Proxy就是解决这些问题的方案,Go Proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,使用Go Proxy之后,构建时会直接从Go Proxy站点拉取依赖。
- GoProxy的使用
Go Modules通过GOPROXY环境变量控制如何使用Go Proxy。
例:
GOPROXY="https://proxy1.cn, https://proxy2.cn, direct"
GOPROXY是一个Go Proxy 站点URL列表,可以使用direct表示源站。对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下转proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。