Go 的依赖管理

2023年 7月 31日 50.3k 0

对于庞大的项目,为了更好的管理库,我们需要使用依赖管理工具。

Go依赖管理的演进

Go的依赖库主要经过三次迭代,分别为:

  • GOPATH
  • Go Vendor
  • Go Module

依赖管理的主要目的:

  • 根据不同环境不同项目使用不同依赖
  • 控制依赖库的版本

系统包路径

Go语言中为我们提供了很多内置包,如 fmt、os、io、strconv、strings 等。Go 语言的入口 main() 函数所在的包叫 main。

GOPATH

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

//$GOPATH
.
|-- bin // 存放项目编译的二进制文件
|-- pkg // 存放项目编译的中间产物,加速编译
|-- src // 存放项目源码

$GOPATH/src目录下存放一切工程代码(所有.go文件,源代码),工程本身也是一个依赖包。

使用go get,我们能下载最新版本的包到src目录下,但go get无法传达版本信息,导致GOPATH模式没有版本控制的概念。这可能导致在运行Go程序时,无法保证其他人和你期望依赖的第三方库是一样的版本。又或者,如果有多个项目依赖同一个库,但它们分别依赖的是不同的版本,这时候也无法保证项目都能正常编译。

Go Vendor

为了解决依赖版本问题,Go官方提出了vendor机制,就是在每个项目的根目录下有一个vendor目录,其中存放了该项目依赖的package。

.
|-- ...
|-- 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就能触发依赖的解析。

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的伪版本基础版本前缀和语义化版本一样。

依赖单元中的特殊标识符

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:

A -> B -> C

如上所示的关系中,A到C即间接依赖,而A到B是直接依赖关系。

+imcompatible后缀表示引用了一个不规范的模块。当主版本号大于2时,模块路径上应有一个匹配的主版本后缀,如上面的example/lib1/v3。对于同一个库的不同的主版本,需要建立不同的pkg目录,用不同的go.mod文件管理来表明不同主版本的不兼容性。有些依赖包并未遵循这个定义规则,就会被打上+imcompatible标识。

最小版本选择 (Minimal Version Selection)

img{512x368}

这里的最小并非指的最小的某个模块的版本,而是针对某个模块的一系列可用版本中选择的最小的一个。例如上图,假设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 协议的服务器。它会缓存源站中的软件内容,实现了稳定可靠的依赖分发。

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 getgo 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

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论