对于庞大的项目,为了更好的管理库,我们需要使用依赖管理工具。
Go依赖管理的演进
Go的依赖库主要经过三次迭代,分别为:
- GOPATH
- Go Vendor
- Go Module
依赖管理的主要目的:
- 根据不同环境不同项目使用不同依赖
- 控制依赖库的版本
系统包路径
Go语言中为我们提供了很多内置包,如 fmt、os、io、strconv、strings 等。Go 语言的入口 main() 函数所在的包叫 main。
GOPATH
GOPATH是Go中支持的一个环境变量,是项目的工作区。
scss
//$GOPATH
.
|-- bin // 存放项目编译的二进制文件
|-- pkg // 存放项目编译的中间产物,加速编译
|-- src // 存放项目源码
$GOPATH/src
目录下存放一切工程代码(所有.go文件,源代码),工程本身也是一个依赖包。
使用go get
,我们能下载最新版本的包到src目录下,但go get
无法传达版本信息,导致GOPATH模式没有版本控制的概念。这可能导致在运行Go程序时,无法保证其他人和你期望依赖的第三方库是一样的版本。又或者,如果有多个项目依赖同一个库,但它们分别依赖的是不同的版本,这时候也无法保证项目都能正常编译。
Go Vendor
为了解决依赖版本问题,Go官方提出了vendor机制,就是在每个项目的根目录下有一个vendor目录,其中存放了该项目依赖的package。
lua
.
|-- ...
|-- vendor
基于这个机制,社区开发出了各种版本管理工具,go vendor就是比较流行的一种。这些工具的思路都是为每个项目单独维护一份对应版本依赖的拷贝。在编译时,编译器会先去vendor目录查找依赖,如果没有找到则再去GOPATH目录下查找。这样就解决了GOPATH不同项目依赖不同版本的库的问题,同时完全本地化的编译加快了项目的构建速度。
但此时如果有不同的外部包依赖了不同版本某个包,go vendor无法指定外部包引用的特定版本,而只是在开发时将其拷贝。但一旦外部包升级,vendor下的包也会跟着升级,这对依赖于不同版本的同一包的外部包的升级带来了风险。
Go Module
Go Module是Go在1.11版本之后官方推出的版本管理工具。现在已经作为Go默认的依赖管理工具。在它的管理下,包不再保存在GOPATH中,而是被下载到了$GOPATH/pkg/mod
路径下。Go Module通过go.mod
文件管理依赖的包和版本,通过go mod
或之前的go get
指令工具管理依赖包。
至此,Go Module定义了版本规则和管理项目的依赖关系。
Go的依赖管理(Go Mod)
完善的依赖管理一般需要三个要素,分别是:
- 配置文件,用于描述依赖:
go.mod
- 中心仓库,用于管理依赖库:
proxy
- 本地工具:
go get/mod
依赖配置
go.mod
在项目根目录下通过go mod init [module_path]
会生成一个go.mod
文件。这个文件中标识了我们的项目的依赖的 package 的版本。执行go mod init
时暂时还没有管理项目依赖,只需随后执行go run/test/build
就能触发依赖的解析。
javascript
module example/project/app // 依赖管理的基本单元,用于声明module路径
go 1.16 // 原生库的版本
require (
example/lib1 v1.0.2
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
) // 单元依赖
每个依赖单元用模块路径+版本[module_path] [version/pseudo_version]
来唯一标示。
版本规则
- 语义化版本:
${MAJOR}.${MINOR}.${PATCH}
- 基于commit的伪版本:
vX.0.0-yyyymmddhhmmss-[hashcode]
不同的MAJOR版本表示是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块;MINOR版本通常是新增函数或功能,向后兼容;而patch版本一般是修复bug。
基于commit的伪版本基础版本前缀和语义化版本一样。
依赖单元中的特殊标识符
bash
require (
example/B v1.0.2
example/C v1.0.0 // indirect
example/lib1/v3 v3.0.2
example/lib2 v3.2.0+incompatible
)
//indirect
后缀表示go.mod
对应的当前模块下没有直接导入该依赖模块的包,也就是非直接/间接依赖的意思。例如假设该模块为A:
css
A -> B -> C
如上所示的关系中,A到C即间接依赖,而A到B是直接依赖关系。
+imcompatible
后缀表示引用了一个不规范的模块。当主版本号大于2时,模块路径上应有一个匹配的主版本后缀,如上面的example/lib1/v3
。对于同一个库的不同的主版本,需要建立不同的pkg目录,用不同的go.mod
文件管理来表明不同主版本的不兼容性。有些依赖包并未遵循这个定义规则,就会被打上+imcompatible
标识。
最小版本选择 (Minimal Version Selection)
这里的最小并非指的最小的某个模块的版本,而是针对某个模块的一系列可用版本中选择的最小的一个。例如上图,假设D模块的最新版本是v1.4.2,Go最终会从当前所需D模块的版本集合中(v1.0.6,v1.2.0,v1.3.2)选择该集合中的最新版本(v1.3.2),而选择的这个版本实际上指的是可用的最小版本。个人理解就是取版本集合的上确界。
依赖分发
依赖分发用于表示可以使用何种方式获取依赖,也就是从哪里下载,如何下载的问题。
回源
对于go.mod
中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
但直接使用版本管理仓库下载依赖,存在多个问题:
- 首先无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本
- 无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用
- 大幅增加第三方代码托管平台压力
因此,Go Proxy被用于解决这些问题。
Go Proxy
模块代理是一个遵循GOPROXY
协议的服务器。它会缓存源站中的软件内容,实现了稳定可靠的依赖分发。
ini
GOPROXY = "https://proxy1.cn, https://proxy2.cn, direct"
// proxy1 -> proxy2 -> direct
Go Modules通过GOPROXY
环境变量控制如何使用 Go Proxy;GOPROXY
是一个模块代理站点的URL列表,可以使用direct
表示源站。对于如上配置,整体的依赖寻址路径,会优先从proxy1
下载依赖,如果proxy1
不存在,后在proxy2
寻找,如果proxy2
中也不存在则会回源到源站直接下载依赖,缓存到proxy站点中。
工具
Go Modules中有两个工具,分别是go get
和go mod
。
go get
默认:go get example.org/pkg@update
@update
还可以换成:
- 删除依赖:
@none
- 某个tag版本,语义版本:
@v1.1.2
- 特定的commit:
@23dfdd5
- 某个分支的最新commit:
@main
go mod
- 初始化,创建
go.mod
文件:go mod init
- 下载模块到本地缓存:
go mod download
- 增加需要的依赖并删除不需要的依赖:
go mod tidy