Go Modules 已经成为 Golang 项目依赖管理的标准,它不仅带来了便利,还为开发者们带来了更多的控制和灵活性。在本文中,我们将深入探讨 Go Modules 的精髓,从基础概念到高级技巧,带你领略如何高效管理你的 Golang 项目依赖。无论你是新手还是有经验的开发者,本文都将为你揭开 Go Modules 的神秘面纱,让你在依赖管理的道路上越走越得心应手。
本文依据官方文档解读而来:golang.google.cn/ref/mod
Go 语言的依赖管理经历了三个主要阶段:GOPATH、Go Vendor 和 Go Module。
Go 程序被组织到 Go 包中,Go 包是同一目录中一起编译的 Go 源文件的集合。在一个源文件中定义的函数、类型、变量和常量,对于同一包中的所有其他源文件可见。模块是存储在文件树中的 Go 包的集合,并且文件树根目录有 go.mod 文件。go.mod 文件定义了模块的名称及其依赖包,通过导入路径和版本描述一个依赖。
模块: 在Go语言中,模块是指包含 go.mod 文件的目录。模块是一起发布、版本控制和分发的包的集合。模块可以直接从版本控制仓库或模块代理服务器下载。开发者可以将项目拆分成多个模块,每个模块都有自己的依赖关系和版本控制。
包: 模块下的每个包都是一系列同目录下、将被编译到一起的文件集合。每个模块可以包含一个或多个 Go 包,这些包可以是相关的代码库或应用程序。
模块路径:就是模块的规范名称,被 go.mod 文件中的 module 指令所声明;通常描述模块做什么以及在哪里找到它;模块路径由存储库根路径、存储库中的目录和主要版本后缀(仅适用于主要版本为 v2 或更高)组成。
golang.org/x/net
模块。golang.org/x/tools/gopls
表示的模块在存储库根路径 golang.org/x/tools
的子目录 gopls 下,因此它的模块路径具有子目录 gopls。(模块下还有子模块)/v2
这样的主版本后缀。例如:路径为 github.com/go-playground/assert/v2
的模块。注:国内访问 github.com/golang/tool…
包路径: 是模块路径和包含包的子目录拼起来的结果。比如,模块 "golang.org/x/net" 包含了目录 html 下的包。则这个包路径就是 "golang.org/x/net/html"。
一个版本标示着模块的不可变快照,每个版本都以字母 v 开头,跟着语义版本,一个语义版本由三个非负整数(主要、次要和补丁版本,从左到右)用点分隔组成,例如:v1.2.3。
补丁版本后面可以跟一个以连字符开头的标识符,例如 -pre 或 -beta 表示是一个测试版或者预发布版,它可能包含了一些新的特性和功能,但也可能存在一些已知的问题和限制,通常用于在正式发布之前提供给开发者进行测试和评估。
如果一个版本的主要版本是 0 或者它有一个预发布后缀,那么它就被认为是不稳定的。 不稳定的版本不受兼容性要求的限制。 例如,v0.2.0 可能与 v0.1.0 不兼容,v1.5.0-beta 可能与 v1.5.0 不兼容。
伪版本是一种特殊格式的 “预发布” 版本,对版本控制仓库中特定修订的信息进行编码。 例如:v0.0.0-20191109021931-daa7c04131f5。
每个伪版本有三个部分:
每个伪版本可以是以下三种形式中的一种,具体取决于基础版本。这些形式确保伪版本比其基础版本高,但低于下一个标记的版本.
具有已知基本版本的伪版本排序高于那些版本,但低于后续版本的其他预发布版本. 具有相同基本版本前缀的伪版本按时间顺序排序。
主版本从第 2 个版本开始,模块路径必须有个类似 /v2 的后缀,以此来匹配主版本。例如:如果一个模块在 1.0.0 版本的路径为 example.com/mod
,那么这个模块需要在 v2.0.0 版本时的路径为 example.com/mod/v2
。
如果旧包和新包具有相同的模块路径,那么这个新包需要对旧包向后兼容。
Go Modules 官方认为 v0 版本完全没有兼容性保证,因此不需要区分模块路径,v1 版本默认省略主版本后缀,方便开发者。根据定义,模块在新主版本的包与旧主版本中的包不向后兼容。因此,从 v2 版本开始,包必须使用新的导入路径。这是通过模块路径添加一个主版本后缀实现的(如:/v2)。由于模块路径是模块内每个包的导入路径的前缀,因此向模块路径添加主版本后缀可为每个不兼容版本,提供不同的导入路径。
当一个项目依赖多个模块时,可能会出现版本冲突的情况。例如,模块 A 和模块 B 都依赖于模块 C 的不同版本,这时就会出现版本冲突。Go Modules 提供了一些解决冲突的机制:通常情况下,如果一个模块需要两个不同版本的可传递依赖项,那么应当使用模块的更高版本。但是,如果这两个版本不兼容,那么这两个版本都不能满足所有依赖的需求。Go Modules 通过在模块路径添加一个主版本后缀,将具有不同后缀的模块视为一个独立的模块,解决了不兼容版本依赖冲突的问题,由于包路径=模块路径+包的子目录,因此包的引入路径也是不同的。
但是,如果有一些库不遵守 Go Modules 语义化版本控制的规范(毕竟不是强制的),在一个主版本号内开发了不向后兼容的次要版本,就有可能导致多个依赖项之间的冲突无法解决,比如 v1.2.3 版本定义了 Tools 变量,v1.4.3 版本把这个变量删除或者改名了,就会导致依赖的不兼容冲突。
go.mod 文件中有些库增加了 +incompatible 后缀
例如:github.com/dgrijalva/jwt-go v3.2.0+incompatible
查看该项目的版本,它们只是发布了 v3.2.0 的 tag,并没有包含 +incompatible 后缀。这个后缀是 Go Modules 自己加上去的,用于做一种不兼容的标识:这些库的主要版本号已经大于等于 2,也就是 tag 已经定义到 v2 及以上,正常使用 Go Modules 的库,此时模块路径必须有像 /v2
这样的主版本后缀,用于区分不同主要版本的模块,但它们的模块路径中没有添加类似的后缀,亦或者是压根没有用 Go Modules 进行模块管理,因此无法区分主要版本号不同的模块,而主要版本号不同会被 Go Modules 认为是前后不兼容的,所以 Go Modules 会对这类库打上 +incompatible 后缀。
使用 go env
可以查看 Go 环境变量,这里列一个表格梳理一下重要的环境变量:
GO111MODULE | Go modules 的启用开关;auto:只有项目中包含go.mod 文件,才启用 Go modules,否则关闭 Go modules;on:启用 Go modules,推荐设置;off:禁用 Go modules,不推荐设置。打开方式:go env -w GO111MODULE=on |
---|---|
GOMODCACHE | 存储下载的模块和相关文件的目录。如果没有设置 GOMODCACHE,它默认为 $GOPATH/pkg/mod 。 |
GONOPROXY | 以逗号分隔的模块路径前缀列表(采用 Go 的 path.Match的语法),在这个列表里的模块直接从版本控制仓库下载,不用走代理(如果是私人仓库,代理是访问不到的,就需要配置到这里)。如果不配置,默认值为 GOPRIVATE 环境变量。 |
GONOSUMDB | 以逗号分隔的模块路径前缀列表(采用 Go 的 path.Match的语法),用于绕过 GOSUMDB 指定 Go checksum database 的验证,如果不配置,默认值为 GOPRIVATE 环境变量。 |
GOPRIVATE | 以逗号分隔的模块路径前缀列表(采用 Go 的 path.Match的语法),用来控制 go 命令把哪些仓库看做是私有的仓库,公司内部的私有仓库代码一般不会上传到公共仓库中,因此镜像一般下载不了,此时会从 GOPRIVATE 进行拉取依赖。可以通过go env -w GOPRIVATE="*.example.com" 指令设置私有模块。 |
GOPATH | 在 GOPATH 模式下,GOPATH 变量是一个可能包含 Go 代码的目录列表。Go Modules 模式下 $GOPATH/pkg/mod 存储依赖。 |
GOPROXY | 用于设置 Go 模块代理,用于使 Go 在后续拉取模块版本时能够脱离传统的版本控制系统方式(比如存储在 git 的代码),直接通过镜像站点来快速拉取。GOPROXY 的默认值是:https://proxy.golang.org,direct ,然而国内无法访问,所以必须国内代理地址:[https://goproxy.cn,direct](https://goproxy.cn,direct) ,设置方式:go env -w GOPROXY=https://goproxy.cn,direct 其中 direct 用于指示 Go 回源到模块版本的源地址去抓取,比如依赖地址在 github ,就可以在镜像中抓不到的时候,去 github 拉取。 |
GOSUMDB | 拉取模块依赖时用于安全校验的数据库,默认为 sum.golang.org 在国内也是无法访问的,也是通过模块代理 GOPROXY 进行解决的;它可以保证拉取到的模块版本数据未经过篡改(使用 go.sum 文件实现);绕过特定模块的校验数据库的更好方法是使用 "GOPRIVATE" 或 "GONOSUMDB" 环境变量。 |
Go Modules 模式下有两个重要文件:
go.mod 文件是面向行的,每行包含一个指令,由关键字和参数组成。例如:
module example.com/my/thing
go 1.20
toolchain go1.21.0
require golang.org/x/net v1.2.3
require example.com/new/thing/v2 v2.3.4
exclude golang.org/x/net v1.2.3
replace golang.org/x/net => github.com/golang/net latest
retract [v1.9.0, v1.9.5]
指令一致的相邻行,可以用括号括起来,形成一个块,类似 Go 编码中的 import 语法:
require (
golang.org/x/crypto v1.4.5 // indirect
golang.org/x/text v1.6.7
)
接下来详细介绍一下每个关键字的作用:
module | 模块指令定义主模块的路径,一个 go.mod 文件必须只包含一个模块指令。(前文已经介绍过模块路径的起名规范) |
---|---|
go | 一个 go.mod 文件最多可以包含一个 go 指令,go 指令表示一个模块是按照给定的 Go 版本的语义来编写的;该 go 指令设置使用该模块所需的最低 Go 版本;在 Go 1.21 之前,该指令仅是建议性的,现在这是一个强制性要求。 |
toolchain | 指令 toolchain 声明了与模块一起使用的建议 Go 工具链。不能低于 go 版本。 |
require | 一个 require 指令声明了一个特定模块依赖的最低版本要求。go 命令使用最小版本选择(MVS)解决依赖版本冲突问题;go 命令为某些需求自动添加 //indirect 注释,//indirect 注释表明,该模块为间接依赖,主模块并没有直接引用(import)。(前文已经介绍过版本信息) |
exclude | 一个 exclude 指令可以阻止一个模块的版本被 go 命令加载(后续介绍 MVS 时逻辑会更加清晰)。 |
replace | 替换指令用其他地方的内容替换一个模块的特定版本,或一个模块的所有版本。可以使用另一个模块路径和版本或特定于平台的文件路径来指定替换。 |
retract | retract 指令表示由 go.mod 定义的模块的某个版本或一系列版本不应该被依赖。(retract 指令是在 Go 1.16 中添加的) |
replace
替换指令
**replace github.com/example/xxx => ../github.com/example/xxx**
)module指令定义的模块路径要一致
)。例如:replace golang.org/x/net => github.com/golang/net latest
retract
指令表示由 go.mod 定义的模块的某个版本或一系列版本不应该被依赖。当一个版本过早发布或在发布后发现严重问题时,retract 指令很有用;被撤回的版本应该在版本控制库和模块代理中保持可用,因为还有项目依赖于这个版本。当一个模块的版本被撤回时,go get
, go mod tidy
或其他命令将不会使用自动升级到该版本。依赖于撤回版本的项目可以继续工作,在使用 go list -m -u
或 go get
命令检查更新相关模块时,将被告知模块版本撤回的情况。
举个例子:开发者发布版本 v1.0.0 后,发现 v1.0.0 存在严重 bug,为了防止用户升级到 v1.0.0,可以 retract 向 go.mod 中添加两个指令如下,发布一个 v1.0.1 新版本用于撤回。
retract (
v1.0.0 // Published accidentally. 意外发布(本来为最新,为了防止更新,新发布一个 v1.0.1)
v1.0.1 // Contains retractions only. 此版本为最新,包含 retract,帮助 v1.0.0 撤销
)
当其他项目更新该模块依赖时,比如 go get example.com/m@latest,该 go 命令会从模块的 go.mod 中读取撤回内容 v1.0.1(这是现在的最高版本)。标志着 v1.0.0 都 v1.0.1 被撤回,因此该 go 命令降级到下一个最高版本,也许是 v0.9.5。
retract v1.0.0 // 撤销单个版本
retract [v1.0.0, v1.9.9] // 撤销 v1.0.0 和 v1.9.9 之间的所有版本
retract [v0.0.0, v1.0.1] // assuming v1.0.1 contains this retraction.
retract [v0.0.0-0, v0.15.2] // assuming v0.15.2 contains this retraction.
go.sum 文件中,每行记录由模块名、版本、哈希算法和哈希值组成;文件中记录了所有依赖的 module 的校验信息,以防下载的依赖被恶意篡改,主要用于安全校验。
// 格式
<module> <version> <hash>
<module> <version>/go.mod <hash>
// 例子
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
正常情况下,每个依赖包会包含两条记录,内容详解:
当使用 go get
命令引入依赖包时,包的哈希值会经过校验和数据库(checksum database)进行校验,校验通过才会被加入到 go.sum 文件中。当执行项目构建时,会从本地缓存中查找依赖包,通过计算和校验依赖包的哈希值的方式进行对比,来确保依赖包没有被篡改。
go.sum 文件可以包含模块的多个版本的哈希,go 命令可能需要从多个版本的依赖关系加载 go.mod 文件以执行最小版本选择。go.sum 也可以包含不再需要的模块版本的哈希 (例如,升级后)。 go mod tidy
将添加缺少哈希物,并将从 go.sum 中删除不必要的哈希。
当我们使用大多数模块感知类(会操作 go.mod 文件)指令(如:go get
)时 ,会触发模块的自动更新机制,自动的修复go.mod 和 go.sum 中的问题。在 Go 1.15 及更低版本中,go 指令默认启用了 -mod=mod
参数,因此会触发自动更新; 自 Go 1.16 以来,默认设置为 -mod=readonly,表示如果对go.mod 有修改的需要,会报告错误并建议修复。
Go 使用一种称为最小版本选择 (MVS) 的算法来选择构建包时要使用的一组模块版本。
模块版本有向图:
MVS 从主模块开始(图中没有版本的特殊顶点),遍历图跟踪每个模块所需的最高版本。在遍历结束时,所需的最高版本构成构建列表:它们是满足所有要求的最低版本。可以使用命令 go list -m all 检查构建列表。
考虑下图中的示例。主模块需要模块 A(最低 1.2 版本) 和 模块 B (最低 1.2 版本),A 1.2 和 B 1.2 分别依赖 C 1.3 和 C 1.4,C 1.3 和 C 1.4 都依赖 D 1.2。
MVS 访问并加载所有标注蓝色版本模块的 go.mod 文件(go.mod loaded)。在图上遍历结束时,MVS 返回一个包含粗体版本的构建列表:A 1.2、B 1.2、C 1.4 和 D 1.2(Selected version)。请注意,可以使用更高版本的 B(1.3) 和 D(1.3),但 MVS 不会选择它们,因为不需要它们,选择可用版本内最小的。
在主模块的 go.mod 文件中,可以使用 replace 指令来替换模块内容。因为替换的模块可能依赖不同的版本,替换会更改模块图。
考虑下面的示例,其中 C 1.4 已被 R 替换。R 取决于 D 1.3 而不是 D 1.2,因此 MVS 返回包含 A 1.2、B 1.2、C 1.4(替换为 R)和 D 1.3 的构建列表。
还可以使用主模块 go.mod 文件中的 exclude 指令在特定版本中排除模块。排除也会改变模块图,当某个版本被排除时,它将从模块图中删除,并且对其的要求将被重定向到下一个更高的版本。
考虑下面的例子。C 1.3 已被排除。MVS 将表现为 A 1.2 需要 C 1.4(下一个更高版本)而不是 C 1.3。
go get 命令可以用来升级一组模块。为了执行升级,go 命令在运行 MVS 之前改变了模块图,增加了从访问的版本到升级后的版本。
看下面的例子。模块 B 可以从 1.2 升级到 1.3,C 可以从 1.3 升级到 1.4 ,D 可以从 1.2 升级到 1.3。
升级(和降级)可以增加或删除间接的依赖关系。在这种情况下,E 1.1 和 F 1.1 在升级后出现在构建列表中,因为 E 1.1 是 B 1.3 所需要的。
为了保持升级,go 命令会更新 go.mod 中的依赖版本,它将改变 B 的版本为 1.3 版本;它还将增加对 C 1.4 和 D 1.3 的依赖,并加上 //indirect 注释,表示是因为升级导致的依赖,否则不会选择这些版本。
go get 命令也可以用来降低一组模块的等级。为了执行降级,go 命令通过移除降级后的版本来改变模块图。
考虑下面的例子。 假设发现 C 1.4 存在问题,因此我们降级到 C 1.3,把 C 1.4 从模块图中删除。 因为 B 需要 C 1.4 或更高版本,B 1.2 也被删除, 主模块对 B 的要求改为 1.1。
go get 也可以完全删除依赖项,在参数后使用 @none 后缀。 这与降级类似。指定模块的所有版本都将从模块图中删除。
命令:
go mod init project_name
初始化项目使用 Go Modules 管理依赖,可以在 $GOPATH 以外的目录创建一个任意目录:myGoMod,然后初始化项目,步骤如下:
mkdir myGoMod
cd myGoMod
go mod init myGoMod
执行完毕以上指令,发现 myGoMod 目录下创建了一个 go.mod 文件,内容如下:
module myGoMod
go 1.20
命令:
go get github.com/gin-gonic/gin
拉取依赖,并构建模块go mod tidy
删除未使用的依赖项新建 main.go 文件,写入如下代码,执行 go get 指令获取并构建 gin 模块,不指定版本将拉取最新的版本。
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
执行 go mod tidy
确保 go.mod 文件与模块中的源代码匹配。执行完发现 go.mod 中写入了一堆依赖,只有 require github.com/gin-gonic/gin v1.9.1
是我们项目直接 import 的依赖。
命令:
go list -m -versions github.com/gin-gonic/gin
查看依赖的历史版本。go list -m github.com/gin-gonic/gin
查看依赖的模块信息,或者使用 all 查看所有模块,还可以加入 -json 查看结构化信息(包括依赖的存储缓存地址),-u 查看可以升级的信息。go get github.com/gin-gonic/gin@v1.9.1
参数后面显式地指定版本,用于版本降级和升级。我们具体操作一下:
go list -m -versions github.com/gin-gonic/gin
2. 版本降级:go get github.com/gin-gonic/gin@v1.6.1
,此时发现 go.mod 关于 gin 模块的依赖已经改变,但有部分依赖我们已经不需要了,可以执行 go mod tidy
清理一下。
3. 查看一下具体模块信息:go list -m -u -json github.com/gin-gonic/gin
4. 版本升级就是反向过程:go get github.com/gin-gonic/gin@latest
当涉及到 Golang 项目的包管理时,Go Modules 提供了一种全新的方式来高效管理项目的依赖。作为 Go 语言的官方包管理解决方案,Go Modules 极大地简化了项目的依赖管理流程,使得开发人员能够更加轻松地管理项目所需的第三方包。Go Modules 的核心原理在于模块化,它通过引入模块的概念,使得每个包都可以独立于其他包进行版本控制和管理。每个模块都拥有自己的版本信息,可以明确地指明其依赖关系,这使得依赖管理变得更加清晰和可控。在 Go Modules 中,模块的版本信息是通过语义化版本控制(Semantic Versioning)来管理的,这意味着每个模块版本的变化都具有清晰的语义意义,开发者可以根据实际需求进行版本的升级和降级。除此之外,Go Modules 还引入了 go.sum 文件来记录模块的哈希值,用于确保模块的下载和使用过程中不会被篡改,从而提高了模块的安全性。总的来说,Go Modules 的原理基于模块化和语义化版本控制,它让依赖管理变得更加清晰、可控和安全。通过深入理解 Go Modules 的原理,开发者可以更好地利用这一工具来管理项目的依赖,提高开发效率。以下是 Go Modules 的一些主要优点:
阅读量:514
点赞量:0
收藏量:0