diff --git a/chap-17-go modules/chapter_17_Go modules.md b/chap-17-go modules/chapter_17_Go modules.md new file mode 100644 index 0000000000000000000000000000000000000000..36713892047f13939fdd3eb8bdcaba69b9779663 --- /dev/null +++ b/chap-17-go modules/chapter_17_Go modules.md @@ -0,0 +1,1424 @@ +# Chapter 17: Go modules + + +![](imgs/1_modules.33fb029e.jpg) + +## 1 本章中能学到的内容 + +- 什么是模块? +- 什么是程序依赖,依赖图长什么样? +- Go版本控制的方法。 +- 怎么使用语义版本控制。 +- 模块的基本操作。 + - 升级所有包,升级某个包,降级某个包。 + +### 1.1 本章中提及的技术概念 + +- 依赖 +- 版本 +- 语义版本控制(SemVer) +- 模块(Module) + +## 2 介绍 + +从版本 1.11 开始,Go 引入了模块。可以把开发过程拆分成一个个独立的代码单元,也即模块。模块可以被其他项目重用。 + + +## 3 依赖 + +开发者重用同行的代码一般有以下的益处: + +- 减少开发时间 + - 基本功能已由团队或个人开发完成,没有必要再花时间重复“造轮子” + - “造轮子”的过程无法体现个人创造的价值附加值 + +- 减少维护成本 + - 团队成员编写的代码需要团队自身维护,比如修复 bug,缺陷等 + - 复用活跃社区的代码通常更能得到有效维护: + - 在选择复用代码时,一个重要的考量标准就是社区在项目上的活跃度 + - 一个有着众多代码提交的活跃社区,通常比仅由几个开发者维护的社区会更安全。 + +一个项目通常用到多个其他项目或者库,一个项目引用的的所有项目和库的列叫做项目的依赖。这里我们说项目 **依赖** 其它软件。 +通过模块,开发者可以方便的引用/复用其他开发者的代码。 + + +## 4 模块的定义 + +我们可以把模块看成一组或一个版本化的包。这些个 go 文件组成了模块。一个模块通常依赖其他的模块。 + +- 由称作“module path”(模块路径)的字符串来标识不同的模块。 +- 有关模块的特性说明列在特定的文件: + - Go 程序依据该文件的特性说明来构建模块。 + - 该文件即 go.mod。 + +- Go 程序同时会生产 go.sum 文件,后面会讲到这个文件。 + + +![](imgs/3_modules.3b193265.png) + +模块由一个个包组成,包里含有 go.mod 和 go.sum 文件。 + +## 5 go.mod 文件 + +go.mod 文件的结构如下: + +```go + module gitlab.com/maximilienandile/myAwesomeModule + + go 1.15 + + require ( + github.com/PuerkitoBio/goquery v1.6.1 + github.com/aws/aws-sdk-go v1.36.31 + ) +``` + +- 第一行声明了一个 **模块路径**,这个路径可以是一个本地路径,或一个有版本控制的仓库URI。 +- 开头的 `module` 是 Go 程序的预留关键词。标识这是一个 Go 模块。本例中的模块位于笔者的私人 gitlab.com 仓库中,项目名字是“myAwesomeModule”。 +- 第二行说明了程序使用的 Go 版本,本例是 1.15。 +- 接下来的部分定义了该模块使用的所有依赖,依赖格式: + + ```go + require( + DEPENDENCY_1_PATH VERSION_OF_DEPENDENCY_1 + DEPENDENCY_2_PATH VERSION_OF_DEPENDENCY_2 + //... + ) + ``` + +首先是模块路径,然后是对应的版本: + +```go + github.com/PuerkitoBio/goquery v1.6.1 +``` + + 我们将从 GitHub 上下载该模块,模块名为"github.com/PuerkitoBio/goquery",版本为 v1.6.1。 + +### 5.1 用 go 命令行初始化空项目 + +如下 go 命令行可自动创建 go.mod 文件: + +```go + $ go mod init my/module/path +``` + +创建文件内容: + +```go + module my/module/path + + go 1.15 +``` + +目前为止,我们的 go.mod 文件不包含任何依赖。 +通过 **require** 关键词添加依赖: + + ```go + module gitlab.com/maximilienandile/myawesomemodule + + require ( + github.com/go-redis/redis/v8 v8.4.10 + ) +``` + +本例中名叫 gitlab.com/maximilienandile/myawesomemodulemodule 的模块,依赖于模块 github.com/go-redis/redis/v8,版本是 v8.4.10。 + +### 5.2 非空项目的初始化 + + +对一个已存在的项目,我们也可以通过命令行创建 go.mod 文件。举个例子: + +创建 main.go 文件,该文件引用仓库“gitlab.com/loir42/gomodule”的代码: + +```go + package main + + import "gitlab.com/loir42/gomodule" + + func main() { + gomodule.WhatTimeIsIt() + } +``` + +被引用的“gitlab.com/loir42/gomodule”模块仅有一个对外暴露的接口函数:WhatTimeIsIt(该函数位于 timer 包): + + +![](imgs/tree_view_package_timer.7a58fc4c.png) + +如上是 timer 包的树形图。 + +以下是 timer.go 的内容: + +```go + package gomodule + + import "time" + + func WhatTimeIsIt() string { + return time.Now().Format(time.RFC3339) + } +``` + +我们执行命令行 **go mod init**,将自动创建 go.mod 文件,此时依赖还没有添加进来: + +```go + module go_book/modules/app +``` + +接下来我们在命令行执行 **go build** 来构建项目,构建过程中 go mod 文件将被修改,依赖将被添加到require部分: + +```go + module go_book/modules/app + + require gitlab.com/loir402/gomodule v0.0.1 +``` + + 整个过程中我们没有指定被依赖的模块版本,build (构建)工具从远程仓库为我们检索到最新的版本 tag,更新到 go mod 文件中。 + +### 5.3 排除 + +我们可以在 go.mod 中显式地指定指定排除构建某个版本: + +```go + exclude gitlab.com/loir402/bluesodium v2.0.1 +``` + +也可以指定排除多个版本: + +```go + exclude ( + gitlab.com/loir402/bluesodium v2.0.1 + gitlab.com/loir402/bluesodium v2.0.0 + ) +``` + +### 5.4 替换 + +通过指令“replace”可以把项目依赖的某个模块代码替换成相应模块的代码: + +```go + replace ( + gitlab.com/loir402/bluesodium v2.0.1 => gitlab.com/loir402/bluesodium2 v1.0.0 + gitlab.com/loir402/corge => ./corgeforked + ) + ``` + + “=>”左边是需要被替换掉的模块,右边是替换模块。 + 替换module存储在: + +- **对外共享的网站**:Github, GitLab…… + +- **本地** + +**以下需加注意**: + +- 替换模块应具有**同样的模块指令**,即 go.mod 文件的第一行应该相同 +- **替换模块需要指明版本吗?**取决于替换模块的存储位置: + - 远端(Github,Gitlab):需要 + - 本地:不需要 + +**一个**还是**多个**版本将被替换呢? + +```go + gitlab.com/loir402/corge => ./corgeforked +``` + +被替换模块没有指定版本,“gitlab.com/loir402/corge”的**所有**版本都将被本地的“./corgeforked”替换。 + +```go + gitlab.com/loir402/corge v0.1.0 => ./corgeforked +``` + +指定了版本 v0.1.0,只有**该版本**将被本地代码替换。 + +## 6 API + +API 是 **A**pplication **P**rogramming **I**nterface 的简写。最重要的字母是“I”。API 也称**接口**。 + +API 就像是两个事物的边界。“一个共享的边界,计算机系统中两个或更多独立的组件基于此边界交换信息”(Wikipedia)。 + +计算机科学中,接口是两个不同事物通信的一种方式。那什么是 API 呢?API 就是暴露给软件交互的构建集合(包括常量,变量,函数……)。 +基于以上,我们可以说 fmt 包暴露了一个 API 给 go 开发者交互。这个 API 包含一套我们可以调用的函数,比如说函数 Println。**同时也包含包中暴露的标识符(变量,常量,类型)。** + +我们还可以说 Linux 内核暴露了一个 **API** 供交互。比如函数 bitmap_find_free_region 是告诉内核去寻找一块连续对齐的内存。 + =>Go 模块暴露了这样一个 API:它由构成该模块的包中**暴露**的所有标识符组成。 + +## 7 版本 + +程序日常演进包括: + +- 增加新函数功能(或删除) +- bug 修复(或引入) +- 性能优化(或降低) + +开发者对原始程序进行修订,每次修订都使得不同于原始版本,或更好,或更差。有时候,模块的 API 会这样演进:之前暴露的函数被删除。 + +依赖这些函数的程序会被中断,因为这些函数不再暴露给程序了。 + +开发者需要一个能精准标定代码单元版本的方式1。 + +版本,作为一个**唯一标识符**,能标定**特定修订和时间点**的程序。 + +这些唯一标识符基于一套熟知的共享规则生成。这套规则和标识符的格式被称为版本控制方案。众版本控制方案中,Go 采用的是语义版本控制,我们会在下一章节详细讲述。 + +## 8 标签(tag),修订(revision),预发布(prerelease),发布(release) + +- **Tagged**:版本控制系统创建了一个**tag**。 +- tag 标签是一个**字符串**(名字),它标定版本控制系统中项目特定的修订版本。 + + - 比如:“v1.0.1”是一个 tag + - “v1.0.1”是 tag 名字 + - 它标定了代码仓库中特定修订版的代码 + - tag 可以是**任意字符串** + - **“GoIsMyFavLanguage”**是一个有效的字符串 + + +- tag 可以标定软件的**发布版本**或者**预发布版本** + - 预发布版本(发布候选版本)是准备要发布的版本。再经过最后几次检测就可以发布的版本。 + + - 当然,这个不是稳定版本(甚至还不算 release) + - 预发布版本含有附加字符的特定 tag: + - 比如:1.0.0-alpha, 1.0.0-alpha.1 + - 每个项目依据各自不同的约定 + +- **未标记的版本**对应程序特定时间的状态。 + - 在 Git 版本控制系统中,对应 **commit** + - Git 版本控制系统会对每次 commit 生成唯一的 SHA1 校验和(比如:409d7a2e37605dc5838794cebace764ea4022388)。 + +## 9 语义版本控制 + +语义版本控制2是由 Tom Preston-Werner 编写的一套版本控制规范。Tom3。这套规范定义了版本号生成方式,在开发者社区中广泛使用。 + +在本节中,我会详述语义版本控制的部分特性。 + +### 9.1 版本号格式 + +- 版本号 X.Y.Z 需遵循如下格式: +- X:主版本号 +- Y:次版本号 +- Z:补丁号 + + 格式规范:X, Y和Z都是非负整数(最高位不为零)。版本号依据既定的规则递增。 + +- 增加新的特性,**打破现有的 API** 时,需要增加主版本号 + - 旧版本: 1.0.0 / 新版本 : 2.0.0 + +- 添加新特性、优化性能,但**没有打破现有 API**,需增加次版本号 + - 旧版本: 1.0.0 / 新版本 : 1.1.0 + +- bug 修复,增加补丁号即可 + - 旧版本: 1.0.0 / 新版本 : 1.0.1 + +创建主版本时,把次版本号和补丁号归零。发布新特性时,把补丁号归零。 + + +### 9.2 主版本号 0 + +在项目的预开发阶段,可以很自然预料到的是,项目会发生很多的变化,以至 API 也会改变。那这个阶段我们怎么创建版本呢。我们可以把主版本号归零,次版本号和补丁号根据预开发进度增加。其他开发者在合入我们的版本代码时, 他们会根据主版本号 0 知道,这个是一个预开发版本,是**不稳定版本,自然他们可以在不用通告的情况下改变 API**。 + +### 9.3 保证稳定性的工具 + +当我们增加了主版本号时,我们同时传达了一份讯息:我们已经开发了和之前版本不兼容的变化。 + +举个 API 变化的例子:一个名为 **gomodule** 的模块对外暴露 WhatTimeIsIt 的函数: + +```go + package gomodule + + import "time" + + func WhatTimeIsIt() string { + return time.Now().Format(time.RFC3339) + } +``` + +开发者当前发布的是 0.0.1 版本。后面开发者接到很多关于时间格式的客诉。开发者想要能够指定时间格式。RFC3339 非常酷,但他们想要有所选择。于是一个新的版本准备好了: + +```go + package gomodule + + import "time" + + func WhatTimeIsIt(format string) string { + return time.Now().Format(format) + } + ``` + +很显然,**函数签名**已经改变。因此,如果导入这个新版本,使用这段代码的程序会中断。这就使得开发者不得创建一个新的主版本 1.0.0。 + +### 9.4 删除版本 + +版本一旦创建, 我们就不能再更改对应的软件了。我们不能给软件标定了版本号,然后删除这个软件的版本,再用这个版本号创建一份新的软件。 + + +## 10 依赖图 + +依赖解决是一个复杂的任务,存有很多解决方法。但首先,我有必要定义依赖图的概念。 + +**依赖图**结构化地展现了一个应用运行的所有依赖:依赖和次依赖。 + +![](imgs/dep_graph.627c0be1.png) + +上图表示 myApp 应用的依赖图。每个**节点**(圆圈)代表一个包,节点也称作顶点或者叶子节点。 **myApp** 就是一个节点。 + +两个节点间的连接称作边。边用直线表示。在依赖图中,箭头表示了边的方向。一个直边表示一个依赖。 + +- myApp 直接依赖于两个包:foo,bar + +也把 foo 和 bar 称作 myApp 的子节点。 + +- foo 包没有依赖于任何包。foo 没有子节点。 + +- bar 依赖于两个包 baz 和 qux。 + +- qux 依赖于 corge。 + +- baz 也依赖于 corge。 + +## 11 依赖解决 + +依赖图展示了所需的依赖。要安装包,我们需要所有依赖的清单。创建这样一份最终的依赖清单, 我们需要遵循依赖图的规范。依赖解决就是遵循依赖图创建依赖清单的过程。 + +在我们的例子中,最终的清单如下: + +- Foo +- Bar +- Baz +- Qux +- Corge + +注意:尽管 Corge 被 Baz 和 Qux 共依赖了 2 次,我们仅需要在清单中添加 1 次就可以。 + +## 12 版本选择问题 + +在上面的依赖图中,我们没有涉及到版本号的问题。本节中,我们将加入版本号。 + +![](imgs/dep_graph_version.7b4e7e6f.png) + +上图带版本号的依赖图: + +- myApp v1.0.0 依赖于 foo v1.2.0 和 bar v1.3.8 …… + +可以看到上图中的 corge 节点在本图中分成了两个节点。**baz 和 qux 都依赖于 corge, 且依赖的是不同版本的 corge 包**。 + +那依赖清单上应该展现哪个版本的 corge 呢? + +- v2.0.0 还是 2.7.0? + + +- 两者都展现? + +- 或者较新的版本,这里是 v2.7.0? + +我们将在下一节探究 go 是怎么处理版本选择的。 + + +## 13 Go:最小版本选择 + +最小版本选择(MVS)是被 go 命令行底层所使用的一套算法4集合: + +- 生成列出项目所有依赖模块的 go.mod 和 go.sum文件 +- 更新一个或多个依赖 +- 降级一个或多个依赖 + +MVS 已被 Russ Cox理论化。Russ 意在创建一个易理解,可预测的系统。 +在本节中,我们会通过聚焦我们的日常的主要操作来详述 MVS 是怎么工作的。 + +### 13.1 两个基本原则 + +- 每个模块都需提供一个 依赖项清单: + - 模块被**module path**标识 + - 每个模块需要指定一个**最小兼容版本** + - 这些在 **go.mod** 中列明 + +- 每个模块遵循 “**导入兼容原则**”。 + - 有**同样模块路径**的模块应**向后兼容**。 + +### 13.2 怎样添加依赖 + +当你通过 **go get** 往项目添加新的依赖时,会发生: + +- 下载指定的模块 + +- 把指定模块添加到 go.mod 文件 + +#### 13.2.0.1 哪个版本会被选择 + +Go 会按**以下顺序**选择版本 + + 1. 最新的稳定版本 tag + 2. 最新的预发布版本 tag + 3. 最新的 commit 版本 id + + +### 13.3 build(构建)清单构建 + +- 构建清单是构建 go 程序所需的模块清单。 + +- 清单的每项由如下两项构成: + - 标识模块的模块路径 + - 版本标识(可以是 tag 或 commit id) + +构建一个模块的构建清单的步骤如下5: +1. 初始化空清单 L +2. 列出当前模块依赖的模块清单 +3. 对每一个被依赖的模块 + + 1. 列出这个模块依赖的模块清单 + 2. 把这些被依赖的模块添加到清单 L + 3. 对列表的每个元素重复以上操作 + +4. 最后,列表中可能会存在多个模块对应一个模块路径 + 1. 如果是这样,每个模块路径保留最新版本的模块 + +输入以下命令,可查看模块最终版本的构建清单: + +```go + $ go list -m all +``` + +在下面, 你可以看到一个思维导图: 它展示了一个模块的依赖链,这个模块仅直接依赖模 gitlab.com/loir402/bluesodium/v2 ,版本是v2.1.1。 + +- 我们从 bluesodium v2.1.1 的依赖清单开始 +- 它依赖 goquery + - 依赖 cascadia + - net + - 依赖 crypto,sys 和 text. + - …… + +![](imgs/modules_requirement_mindmap.83a56a6e.png) + + +最后,我们得到如下清单: + + +- github.com/PuerkitoBio/goquery v1.6.1 + +- github.com/andybalholm/cascadia v1.1.0 + +- gitlab.com/loir402/bluesodium/v2 v2.1.1 + +- golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + +- **golang.org/x/net v0.0.0-20200202094626-16171245cfb2** + +- golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a + +- golang.org/x/text v0.3.0 + +- **golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01** + + +加粗线的 net 模块被两个地方依赖: + + +- golang.org/x/net v0.0.0-20200202094626-16171245cfb2 + +- golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01 + +版本**golang.org/x/net v0.0.0-20200202094626-16171245cfb2**会被选择, 因为是**更新的版本**。 + +### 13.4 如何把依赖升级到最新的次版本或补丁 + +我们第一次添加依赖时,Go 会为我们下载特定版本: + +- 最新的发布 tag +- 最新的预发布 tag +- 特定的 commit + +然后当我们想升级依赖到下一个版本时,我们可以输入: + +```go + go get -u gitlab.com/loir402/bluesodium +``` + +选项 `-u` 会为我们下载更新的**次版本**或 **补丁**6 + +### 13.5 升级依赖到新的主版本 + +对于 `module gitlab.com/loir402/bluesodium`,现在项目使用的是版本 v1.0.1 。 + +维护者发布一个新的**主版本**:仓库中会出现以 **V2** 开头的版本 tag。 + + +![](imgs/gitlab_new_tags.cc2ffc37.png) + + +现在你项目中依赖的一个模块已有了新的主版本发布,如果你尝试用以下命令升级: + +```go + go get -u gitlab.com/loir402/bluesodium +``` + +程序将不会下载最新的主版本(v2.0.1),为什么: + +- 版本 2 的模块有不同的模块路径 + +- 这是“**导入兼容原则**”的直观应用: + - 有同样的模块路径的模块都是向后兼容的。 + - 版本 2 的模块引入了重大变化。这些变化会影响之前版本的使用者,因此和之前版本不兼容。 + - 以上,新版本应该有不同的模块路径。 + +我们看一下 go.mod 的依赖: + +```go + module gitlab.com/loir402/bluesodium/v2 + + go 1.15 +``` + +模块路径已经改变了;不再是`gitlab.com/loir402/bluesodium` ,而是 `gitlab.com/loir402/bluesodium/v2`。 + +当模块从 v0 或 v1 切换到 v2,它应该修改模块路径以遵循“导入兼容原则”。 +为了指定下载主版本 2,执行以下命令: +这些变化会影响之前版本的使用者 + +```go + go get -u gitlab.com/loir402/bluesodium/v2 +``` + +### 13.6 升级所有的模块到最新版本 + +- 当构建构建清单的时候,每个依赖都会被看作需要依赖最新的模块版本。 + +由于“导入兼容原则”,重大的变化不应该在次版本被引入(这个操作要求新的补丁和次要版本)。 + +如果新版本被找到,构建清单会被修改,go.mod 也会修改。 + +具体地,输入以下命令: + +```go + $ go get -u ./... + go: github.com/andybalholm/cascadia upgrade => v1.2.0 + go: golang.org/x/net upgrade => v0.0.0-20210119194325-5f4716e94777 +``` + +这个命令将输出被升级的依赖(这里是 cascadia 和 net)。我们看一下 go.mod 文件。 + +```go + module thisIsATest + + go 1.15 + + require ( + github.com/andybalholm/cascadia v1.2.0 // indirect + gitlab.com/loir402/bluesodium/v2 v2.1.1 + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + ) + ``` + +这里你可以看到被升级的模块已被添加到 go.mod 文件,同时也添加了 **indirect** 的注释。为什么呢,这些添加的行能保证构建构建清单的时候我们使用这些行指定的升级版本。不然,构建清单会保持不变。 + +### 13.7 升级模块到指定的新版本 + +这里我们想升级某个模块到最新的版本,升级算法如下: + +- 未升级前,生成的是第一版构建清单。 + +- 指定升级模块后,生成第二个构建清单。 + +- 两个构建清单合并。 + +- 如果某个模块在清单中出现两次,会保留最新的版本。 + +![](imgs/upgrade_one_module_to_specific_newer.dab029d9.png) + +### 13.8 降模块到指定的低版本 + +降级某个依赖是一种常见操作。在依赖出现以下情况时,有必要降级依赖: + +- 引入了 bug +- 引入了安全漏洞 +- 降低了程序性能 +- 一些其他的原因 + +这种情况下,开发者需要能引用依赖的之前版本。 + +比如说,我们先前使用模块 E V1.1.0,现想把版本降级到 v1.0.0。Go 将会检视应用的每个依赖: + +- 为该依赖构建对应的构建清单 +- 如果构建清单包含E的禁止版本(比如说 E v1.1.0 或更高) + - 需求的版本将被降级 + - 如果降级的版本还是包含 v1.1.0 或更高,将往下一个版本降级 + - 降级会一直重复,直到构建清单中没有禁止版本 + +降级操作也可能会移除不再需要的依赖。 + +### 13.9 排除模块 + +在前面的章节中,我们已经接触到 go.mod 文件可以排除模块的特定版本(通过"exclude"指令)。 + +排除情况下,Go 会搜索模块下一个更新的版本(该版本不能是被排除的)。会在清单中搜索: + +- 发布版本 +- 预发布版本 + +注意到在这个过程中不会搜索 commits。 + +## 14 校验和 + +- 校验和是以字符,文件等作为输入,通过哈希算法产生的字符的集合。 + +- 校验和的目的之一是快速校验通过网络传输的数据的完整性。 + - 通过网络传输的数据并非 100% 安全,由于网络或者黑客的攻击,有时我们会得到损坏的文件。 + - 如果你把文件作者产生的校验和同自己生成的文件校验和比较,你基本可以坚信你和作者比较的是同一份文件。 + - 这里我说基本坚信是因为,哈希算法也存在不健壮的时刻,这个时刻对两个不同的文件会产生一样的校验和。比如说,哈希碰撞的时刻,MD5 算法就会对于两个不同的文件产生相同的校验和。 + +## 15 哈希 vs 编码 vs 加密 + +哈希是由哈希算法生成的字符串。哈希**不同于**加密和编码: + +- 哈希: + - 一个哈希算法输出大小相同的哈希散列。 + - 文件,字符串,zip 等都可是哈希的输入。 + - 理论上,不能倒推得到哈希的输入。 + +- 编码 + - 是把一种格式的数据转换成另一种格式的过程。 + - 可由输出倒推回输入。 + + +- 加密 + - 是一个把通常是明文的输入,转换为密文(输出)的过程。 + - 没有密钥的情况下,解不开密文。 + - 有授权才可以从密文倒推回明文。 + - 得到授权需要有密钥。 + - 密钥可以是对称或非对称的。 + +## 16 go.sum 文件 + +go.sum 文件包含模块**直接**依赖和**间接**依赖的加密散列7。 + +### 16.1 举个例子 + +我们初次为项目 myApp 生成 go.mod 文件,在终端输入: + +```go + $ cd myApp + $ go mod init +``` + +生成 go.mod 文件如下: + +```go + module gitlab.com/loir402/myApp +``` + +这是个空文件,`go mod init` 命令并不会为我们在 go.mod 中添加依赖。我们可以执行下面的命令来自动添加: + +```go + $ go install +``` + +可以看到 go.mod 文件更新了依赖,同时创建了 go.sum 文件。 + +```go + // myApp/go.mod + module gitlab.com/loir402/myApp + + require ( + gitlab.com/loir402/bar v1.0.0 + gitlab.com/loir402/foo v1.0.0 + ) + ``` +go.mod 列出了两个直接依赖:foo 和 bar。系统为两个模块自动选择的最新版本都是 v1.0.0。 + +生成的 **go.sum** 文件如下: + + gitlab.com/loir402/bar v1.0.0 h1:l8z9pDvQfdWLOG4HNaEPHdd1FMaceFfIUv7nucKDr/E= + gitlab.com/loir402/bar v1.0.0/go.mod h1:i/AbOUnjwb8HUqxgi4yndsuKTdcZ/ztfO7lLbu5P/2E= + gitlab.com/loir402/baz v1.0.0 h1:ptLfwX2qCoPihymPx027lWKNlbu/nwLDgLcfGybmC/c= + gitlab.com/loir402/baz v1.0.0/go.mod h1:uUDHCXWc4HmQdep9P0glAYFdIEcenfXwuHmBfAMaEgA= + gitlab.com/loir402/corge v1.0.0 h1:UrSyy1/ZAFz3280Blrrc37rx5TBLwNcJaXKhN358XO8= + gitlab.com/loir402/corge v1.0.0/go.mod h1:xitAqlOH/wLiaSvVxYYkgqaQApnaionLWyrUAj6l2h4= + gitlab.com/loir402/foo v1.0.0 h1:sIEfKULMonD3L9COiI2GyGN7SdzXLw0rbT5lcW60t84= + gitlab.com/loir402/foo v1.0.0/go.mod h1:+IP28RhAFM6FlBl5iSYCGAJoG5GtZpUH4Mteu0ekyDY= + gitlab.com/loir402/qux v1.0.0 h1:B1efJPpCgzevbS5THHliTj1owKfOi0Yo7tIaAm65n6w= + gitlab.com/loir402/qux v1.0.0/go.mod h1:QexiArTQZcXRpFC3LLuGhk82aJoknf1n6c4WxlTeWdg= + +### 16.2 go.sum 结构剖析 + +go.sum 列出了项目的所有依赖,包含直接依赖和间接依赖。每个依赖在 go.sum 中对应两行。我们以包 foo 为例: + +```go + gitlab.com/loir402/foo v1.0.0 h1:sIEfKULMonD3L9COiI2GyGN7SdzXLw0rbT5lcW60t84= + gitlab.com/loir402/foo v1.0.0/go.mod h1:+IP28RhAFM6FlBl5iSYCGAJoG5GtZpUH4Mteu0ekyDY= +``` + +两行的结构如下: + +![](imgs/go_sum.37f26b1e.png) + +go.sum 不仅记录引用模块相应版本的校验和,也记录模块对应 go.mod 文件的校验和。这就是每个模块在 go.sum 中的两行记录。 + + 哈希算法是 **SHA256**。 + +- 第一个校验和是模块所有文件的哈希 + +- 第二个校验和是模块的 go.mod 文件的哈希 + + 接下来把哈希转换成 base64。特定字符串 h1 表示采用 go 库中的 Hash1 函数(你可以阅读生成校验和的代码来满足你的好奇心,代码地址是 https://github.com/golang/go/blob/master/src/cmd/go/internal/dirhash/hash.go)。译者注:如果地址 https://github.com/golang/go/blob/master/src/cmd/go/internal/dirhash/hash.go 打不开, 可以尝试地址 https://cs.opensource.google/go/x/mod/+/refs/tags/v0.4.2:sumdb/dirhash/hash.go。 + + +- 校验和保证我们多次下载模块指定版本,获取到的是完全一样的内容。 + +## 17 例子 + +我在 Gitlab 上创建了以下 6 个项目: + +- https://gitlab.com/loir402/myApp + +- https://gitlab.com/loir402/foo + +- https://gitlab.com/loir402/bar + +- https://gitlab.com/loir402/baz + +- https://gitlab.com/loir402/qux + +- https://gitlab.com/loir402/corge + +### 17.1 项目设置 + +我的第一个项目 myApp,也是主项目,有两个直接依赖:foo 和 bar,其他的都是间接依赖。 + +myApp 代码如下: + +```go + // modules/example/main.go + package main + + import ( + "fmt" + + "gitlab.com/loir402/bar" + "gitlab.com/loir402/foo" + ) + + func main() { + fmt.Println(foo.Foo()) + fmt.Println(bar.Bar()) + } +``` + +在 myApp 中我们调用了 foo 和 bar 的 API。包 foo 没有其他依赖,代码如下: + + // foo/foo.go + package foo + + func Foo() string { + return "Foo" + } + +包 bar 有两个直接依赖: baz 和 qux : + +```go + // bar/bar.go + package bar + + import ( + "fmt" + + "gitlab.com/loir402/baz" + "gitlab.com/loir402/qux" + ) + + func Bar() string { + return fmt.Sprintf("Bar %s %s", baz.Baz(), qux.Qux()) + } +``` + +包 qux 如下: + +```go + // qux/qux.go + package qux + + import ( + "fmt" + + "gitlab.com/loir402/corge" + ) + + func Qux() string { + return fmt.Sprintf("Qux %s", corge.Corge()) + } + ``` + +包 baz: + +```go + // baz/baz.go + package baz + + import ( + "fmt" + + "gitlab.com/loir402/corge" + ) + + func Baz() string { + return fmt.Sprintf("Baz %s", corge.Corge()) + } + ``` + +包 corge 被包 baz 和 qux 依赖: + +```go + // corge/corge.go + package corge + + func Corge() string { + return "Corge" + } +``` + +注意到包 core 不依赖其他模块。 + +我在 Gitlab 上为每个包都创建了 **v1.0.0** 版本。 + +### 17.2 把所有依赖升级到最新版本 + +执行以下命令,可以把所有依赖升级到最新版本(**次要版本和补丁版本**): + +```go + $ go get -u ./... +``` + +我们测试一下: + +我们为模块创建一个补丁版本。如果对前面语义版本控制章节还有印象,你会知道,在这里,补丁版本不改变模块的 API(API 也看作模块对外暴露供交互的集合)。 + +```go + // corge/corge.go + package corge + + func Corge() string { + return fmt.Sprintf("Corge") + } +``` + +函数签名不变,我们仅增加了一个 `fmt.Sprintf` 调用。 +我在 Gitlab 上为这个补丁创建了相应版本,版本号 V1.0.1 (增加了末尾的补丁号)。 + +然后我 在myApp 目录下执行 `go get -u`,**myApp** 间接依赖 **corge**: + +```go + $ go get -u ./... + go: finding gitlab.com/loir402/corge v1.0.1 + go: downloading gitlab.com/loir402/corge v1.0.1 +``` + +可以看到,Go 检测到了 corge 发布的新补丁版本,并为我们下载了该版本,go.sum 更新如下: + +```go + //... + gitlab.com/loir402/corge v1.0.0 h1:UrSyy1/ZAFz3280Blrrc37rx5TBLwNcJaXKhN358XO8= + gitlab.com/loir402/corge v1.0.0/go.mod h1:xitAqlOH/wLiaSvVxYYkgqaQApnaionLWyrUAj6l2h4= + gitlab.com/loir402/corge v1.0.1 h1:F1IcYLNkWk/NiFtvOlFrgii2ixrTWg89QarFKWXPRrs= + gitlab.com/loir402/corge v1.0.1/go.mod h1:xitAqlOH/wLiaSvVxYYkgqaQApnaionLWyrUAj6l2h4= + //... +``` +go.mod 文件: + +```go + module gitlab.com/loir402/myApp + + require ( + gitlab.com/loir402/bar v1.0.0 + gitlab.com/loir402/corge v1.0.1 // indirect + gitlab.com/loir402/foo v1.0.0 + ) +``` + +可以看到: + +- go.sum 文件依然记录有 corge 的历史版本 v1.0.0 + +- go.mod 文件中新加了一行:“gitlab.com/loir402/corge v1.0.1” + +有时候我们升级了新版本后,需要降级回历史版本(比如新版本引入了 bug 等)。 +go.sum 保留历史版本记录这一安全机制,就可以保证我们在降级的时候,能降级到之前运行正常的历史版本。 + +### 17.3 升级依赖到最新版本 + +如果不需更新所有依赖,可以通过 `go get` 指定更新某个依赖。 +比如我新更新发布了一个 foo 的补丁版本,更改如下: + +```go + // foo/foo.go + // v1.0.1 + package foo + + func Foo() string { + return fmt.Sprintf("Foo") + } +``` + + +可在命令行执行如下命令,把 foo 更新到最新版本: + +```go + $ go get gitlab.com/loir402/foo + go: finding gitlab.com/loir402/foo v1.0.1 + go: downloading gitlab.com/loir402/foo v1.0.1 +``` +go.mod 文件被更新了,会把最新依赖的 foo 版本 v1.0.1 添加上去: + +```go + module gitlab.com/loir402/myApp + + require ( + gitlab.com/loir402/bar v1.0.0 + gitlab.com/loir402/corge v1.0.1 // indirect + gitlab.com/loir402/foo v1.0.1 + ) +``` + +go.sum 文件更新如下: + +```go + gitlab.com/loir402/foo v1.0.0 h1:sIEfKULMonD3L9COiI2GyGN7SdzXLw0rbT5lcW60t84= + gitlab.com/loir402/foo v1.0.0/go.mod h1:+IP28RhAFM6FlBl5iSYCGAJoG5GtZpUH4Mteu0ekyDY= + gitlab.com/loir402/foo v1.0.1 h1:6Dcvy69SCXzrGshVRDZzswqiA5Qm0n6Wt5VLOFtYF5o= + gitlab.com/loir402/foo v1.0.1/go.mod h1:+IP28RhAFM6FlBl5iSYCGAJoG5GtZpUH4Mteu0ekyDY= +``` + +为了降级考虑,历史版本没有从文件中删除,同时新的版本被添加进来了。 + +### 17.4 把依赖升级到指定版本 + +用以下 `go get` 命令升级到指定版本: + +```go + $ go get module_path@X +``` +这里 X 可以是: + +- commit 哈希 + - Ex : b822ebd + +- 版本号: + - v1.0.3 + +Go 会获取指定的版本并下载到本地。举个例子,我修改了 bar 模块: + +```go + // bar/bar.go + package bar + + import ( + "fmt" + + "gitlab.com/loir402/baz" + "gitlab.com/loir402/qux" + ) + + func Bar() string { + return fmt.Sprintf("Bar %s %s", baz.Baz(), qux.Qux()) + } + + func Bar2() string { + return fmt.Sprintf("Bar2 %s %s", baz.Baz(), qux.Qux()) + } +``` + +我在 API 中增加了一个对外暴露的函数 Bar2,创建了一个新的次版本号:v1.1.0. + +我们同步更新 myApp 使用这个新版本的 Bar。 + +```go + $ go get gitlab.com/loir402/bar@v1.1.0 + go: finding gitlab.com/loir402/bar v1.1.0 + go: downloading gitlab.com/loir402/bar v1.1.0 +``` +我们再看一下 go.mod 文件发生了什么: + +```go + module gitlab.com/loir402/myApp + + require ( + gitlab.com/loir402/bar v1.1.0 + gitlab.com/loir402/corge v1.0.1 // indirect + gitlab.com/loir402/foo v1.0.1 + ) +``` + +模块 bar 的新版本 V1.1.0 已更新到 go.mod。再看 go.sum: + +```go + gitlab.com/loir402/bar v1.0.0 h1:l8z9pDvQfdWLOG4HNaEPHdd1FMaceFfIUv7nucKDr/E= + gitlab.com/loir402/bar v1.0.0/go.mod h1:i/AbOUnjwb8HUqxgi4yndsuKTdcZ/ztfO7lLbu5P/2E= + gitlab.com/loir402/bar v1.1.0 h1:VntceKGOvGEiCGeyyaik5NwU+4APgyS86IZ5/hm6uEc= + gitlab.com/loir402/bar v1.1.0/go.mod h1:i/AbOUnjwb8HUqxgi4yndsuKTdcZ/ztfO7lLbu5P/2E= +``` + +新版本已经更新到清单(同时也保留了旧版本的记录)。 + +### 17.5 依赖降级 + +有时候新升级的版本表现不稳定,在运行的时候发生了新的 bug。这种情况下,我们需要降级回历史运行正常的版本。 + +执行以下命令降级版本: + +```go + $ go get gitlab.com/loir402/bar@v1.0.0 +``` +回退到 bar 的历史版本 v1.0.0 + +## 18 发布版本前的清理工作 + +在发布版本前,我们要确保 **go.mod** 和 **go.sum** 文件和项目的实时依赖保持一致。举个例子:我们先在 myApp 中引入了依赖 grault 的代码,随后我们又删除了这个引用,我们看下这个过程中,go.sum 和 go.mod 有哪些变化。 + +grault 的代码结构和其他测试用的模块一样,它暴露一个回显字符串“Grault”的方法 Grault,其他不变: + +```go + // grault/grault.go + // v1.0.0 + package grault + + import "fmt" + + func Grault() string { + return fmt.Sprintf("Grault") + } + + 在myApp中引入: + + // myApp/main.go + package main + + import ( + //... + "gitlab.com/loir402/grault" + ) + + func main() { + //... + fmt.Println(grault.Grault()) + } +``` +当我们运行 **go install**,go.mod 和 go.sum 都会被加入新的一行。go.mod 如下: + +```go + module gitlab.com/loir402/myApp + + require ( + gitlab.com/loir402/bar v1.0.0 + gitlab.com/loir402/corge v1.0.1 // indirect + gitlab.com/loir402/foo v1.0.1 + gitlab.com/loir402/grault v1.0.0 + ) +``` +现在我们删除 grault 的调用,重新 `go install`。 go.mod 和 go.sum 文件没有变化,仍保留有 grault 的记录。 +而我们的项目已不再引用 grault了。为了清理这个历史记录,我们可以执行: + +```go + $ go mod tidy -v + + unused gitlab.com/loir402/grault +``` + +这会: + +- 把 go.mod 和 go.sum 文件中 grault 的记录移除 + +- 把 go.sum 中不再引用的模块版本移除 + +如果我们把 go.sum 中指定依赖版本的行删除了,日后降级就不一定能回退到删除的版本。当我们降级时,我们需要确保能回退到历史的可用版本。go.sum 文件的加密校验和正好可以保证这一点。 + +## 19 下载的依赖模块存放在哪里 + +Go 把下载模块的各个版本存放在 **$GOPATH/pkg/mod** 目录。下面的清单展示了 mod 文件夹的的树形结构: + +![](imgs/tree_cache_loir402.d8a3d2d0.png) + + +## 20 go.sum 是锁文件吗 + +对于有多语言经验的 go 开发者来说,go 看起来像其他语言依赖系统中的锁文件。锁文件列明了项目的所有依赖和依赖的特定版本号(tag 或 commit)。 + +以下是 Nodejs 语言中的锁文件: + +```js + { + "name": "nodeLock", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + } + } + } + ``` + +这里只有一个叫“moment”的依赖,版本为 **v2.22.2**。 + +以下是一个 php 项目的锁文件(用 composer 作为依赖管理): + +```php + { + //... + "packages": [ + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + //.... + } +``` + +go.sum 文件的不同在于: + +- go.sum 记录了项目直接依赖和间接依赖的版本,和**加密**校验和。 + - 目的是确保下次下载时,模块没有**被改变**。 + - 锁文件的目的是为了多次构建。 + +- Go 有一套**明确**的版本选择机制: + - 如果开发者没有删除版本的话,构建清单就是稳定的。 + - 这意味着一月份生成的构建清单和十二月生成的没什么不同。 + - 因而这里用不上锁文件。 + +- 使用锁文件的依赖管理系统一般没有这个机制。 + +- 锁文件通过列出依赖和指定的版本,保证应用可多次构建。 + + +## 21 Go.sum & go.mod:需要提交吗 + +我们需要把 go.mod 和 go.sum 提交到 VCS(比如 Git),因为: + +- 其他开发者需要 go.mod 文件来构建构建清单。 +- 其他开发者通过 go.sum 文件保证下载的模块是没有被改变的。 + + +## 22 其他命令 + +还有一些其他你应该知道的有趣的命令。本节聚焦以下笔者感兴趣的一些命令。 + +### 22.1 go mod 图 + +go mod 图会在标准输出显示模块的依赖图。 + +以下是 myApp 的依赖图: + +```go + gitlab.com/loir402/myApp gitlab.com/loir402/bar@v1.0.0 + gitlab.com/loir402/myApp gitlab.com/loir402/corge@v1.0.1 + gitlab.com/loir402/myApp gitlab.com/loir402/foo@v1.0.1 + gitlab.com/loir402/bar@v1.0.0 gitlab.com/loir402/baz@v1.0.0 + gitlab.com/loir402/bar@v1.0.0 gitlab.com/loir402/qux@v1.0.0 + gitlab.com/loir402/qux@v1.0.0 gitlab.com/loir402/corge@v1.0.0 + gitlab.com/loir402/baz@v1.0.0 gitlab.com/loir402/corge@v1.0.0 +``` + +虽然不是很形象,但已可以窥见 myApp 的依赖信息。在下图中,我用箭头和圆圈表现依赖。两个圆圈(包)以及中间的边对应上图中的一行依赖。图中:有七个边和六个模块(foo, bar, baz, qux, corge 和 myApp)。corge 出现了两次, 因为 myApp 依赖 corge v1.0.1, 而 qux 和 baz 依赖 corge v1.0.0。 + + +![](imgs/go_mod_graph.eeda79c9.png) + +### 22.2 go mod vendor + +命令 **go mod vendor** 会: +- 为所有依赖的源文间创建 vendor 文件夹。 + +这里是 myApp 的 vendor 文件夹的树形图: + +![](imgs/tree_cendor_go_module.65092c6c.png) + + +vendor 文件夹也包含文件: modules.txt 。该文件列明了所有依赖的路径和版本号。 + +```go + # gitlab.com/loir402/bar v1.0.0 + gitlab.com/loir402/bar + # gitlab.com/loir402/baz v1.0.0 + gitlab.com/loir402/baz + # gitlab.com/loir402/corge v1.0.1 + gitlab.com/loir402/corge + # gitlab.com/loir402/foo v1.0.1 + gitlab.com/loir402/foo + # gitlab.com/loir402/qux v1.0.0 + gitlab.com/loir402/qux +``` + +### 22.3 go mod verify + +从命令名可以看到,`go mod verify` 会检查本地存放的依赖。在应用构建时,Go 检测本地存放的依赖是否有变,以保证构建使用正确的下载版本。`go mod verify `可以检测你使用的是下载版本,不是本地修改过的版本。如果版本有修改,构建会报错。 + +命令行工具会检测存放在 **pkg/mod/cache/download** 下的构建清单每个模块的版本文件,是没有被修改的。以下是 **pkg/mod/cache/download ** 文件的结构: + +![](imgs/tree_view_chache_download.64987ec9.png) + + +可以看到: + Go 存放了应用依赖的每个版本。对于每个版本,都有如下四个不同的文件: +- VERSION.info + +- VERSION.mod + +- VERSION.ziphash + +- VERSION.zip + + info 文件包含下载的版本号信息和时间: + + {"Version":"v1.0.0","Time":"2018-11-03T19:36:07Z"} + +.mod 文件是模块原始 .mod 文件的拷贝版本。.ziphash 文件包含 .zip文件 的哈希。 + +我试着修改模块的 zipped 版本,然后执行 `go mod verify`。如下是 go 返回的错误信息: + +```go + gitlab.com/loir402/foo v1.0.1: zip has been modified (/Users/maximilienandile/go/pkg/mod/cache/download/gitlab.com/loir402/foo/@v/v1.0.1.zip) +``` + +## 23 自我测试 + +### 23.1 问题 + +1. 次版本会引入重大变化。对或错? +2. 把模块更新到最新版本的命令是? +3. Go 管理依赖的算法名是? +4. 用词: packages(包), source files(源文件),go.mod 和 go.sum 组一个定义模块的句子。 +5. go.mod 文件的作用? +6. go.sum 文件的作用? +7. 初始化一个模块的命令? +8. 列出模块的构建清单的命令? +9. 发布一个主版本 V2,不需要修改模块路径,对或错? + + +### 23.2 答案 + +1. 次版本会引入重大变化。对或错? + 1. 错 + 2. 主版本引入重大变化。 + +2. 把模块更新到最新版本的命令是? + `$ go get -u path/of/the/module` +3. Go 管理依赖的算法名是? + 1. MVS: 最小版本选择 + +4. 用词: packages(包),source files(源文件),go.mod 和 go.sum 组一个定义模块的句子。 + 1. 模块由一个或多个包以及 go.mod 和 go.sum 两个文件组成。 + +5. go.mod 文件的作用? + 1. go.mod 定义当前模块的**模块路径** + 2. 列明**直接依赖**的**最小版本** + 3. 列明**间接依赖**的**最小版本**: + 1. 开发者可能也会手动升级依赖,或者移除依赖 + 4. **模块路径**标识不同的依赖 + 5. 列明模块匹配的 go 版本 + +6. go.sum 文件的作用? + 1. go.sum 确保下载依赖的版本文件和仓库源头**保持一致**。 + +7. 初始化一个模块的命令? + 1. 在模块目录: + `go mod init path/of/the/module` +8. 列出模块的构建清单的命令? + 1. 在模块目录: + `go list -m all` + +9. 发布一个主版本 V2,不需要修改模块路径,对或错? + 1. 错 + 2. 基于“导入兼容原则”,因为新的主版本引入了重大变化,因此 module path 应当改变 + 3. 模块路径应增加"v2" + + +## 24 要点 + +- 模块是一组包,这些包在版本控制系统追踪,例如 Git + +- 模块路径标识一个模块: + - 模块路径描述了模块提供的功能,以及从哪里可以下载 + +- 描述版本变化信息的 tag 标识一个版本。 + +- 开发者通常需共享一套版本方案,来描述版本引入的变化。 + +- Go 使用语义版本控制作为版本方案: + - 在这个方案中,版本号是“vX.Y.Z”格式的字符串,"v"可选。 + - X,Y 和 Z 是非负整数 + - X 是主版本号,引入重大变化时,需增加主版本号 + - Y 是次版本号,引入非重大变化时,需增加次版本号 + - Z 是补丁号,补丁版本发布时,需增加补丁号(比如说 bug 修复) + +- 当发布一个大于等于 2 的主版本时,需在模块路径添加**主版本号**前缀 + + - **gitlab.com/loir402/bluesodium** 变成 **gitlab.com/loir402/bluesodium/v2** + + +通过执行 `go mod init my/new/module/path command` 可以初始化已创建项目的一个模块 + + +通过 **go get** 命令给项目添加新的直接依赖: + +```go + $ go get my/new/module/to/add +``` + +通过 **go get** 命令升级所有依赖: + +```go + $ go get -u ./... +``` + +通过 **go get** 命令升级某个依赖: + +```go + $ go get -u the/module/path/you/want/to/upgrade +``` + +通过 **replace**,把模块的代码替换为令一个模块代码(存放在本地或远程仓库的代码) + +```go + replace gitlab.com/loir402/corge => ./corgeforked + ``` + +指定排除构建模块某个版本 + +```go + exclude gitlab.com/loir402/bluesodium v2.0.1 +``` + +1. https://en.wikipedia.org/wiki/Software_versioning [↩︎](#jump1) + +2. https://semver.org/ [↩︎](#jump2) + +3. Tom 是 Github 的联合创始人之一 [↩︎](#jump3) + +4. 在这章节,我们会看到 MVS 是如何工作的。它是Russ Cox’s的著作 [ [@minimal-version-cox]](#jump8)的一个誊写。 [↩︎](#jump4) + +5. 请注意这个算法的实际实现和以上所述的有差别。实际实现基于图的遍历,比以上方法效率更高一点。 可以参看 Russ Cox 的文章了解到更多! [↩︎](#jump5) + +6. 来源: go help get [↩︎](#jump6) + +7. https://golang.org/ref/mod section go.sum [↩︎](#jump7) + + + +参考文献 + +- [hejderup2018software] Hejderup, Joseph, Arie van Deursen, and Georgios Gousios. 2018. “Software Ecosystem Call Graph for Dependency Management.” In 2018 IEEE/ACM 40th International Conference on Software Engineering: New Ideas and Emerging Technologies Results (ICSE-NIER), 101–4. IEEE. +- [li2003managing] Li, Bixin. 2003. “Managing Dependencies in Component-Based Systems Based on Matrix Model.” In Proc. Of Net. Object. Days, 2003:22–25. Citeseer. +- [minimal-version-cox] Cox, Russ. 2018. “Minimal Version Selection.” https://research.swtch.com/vgo-mvs.pdf. + + diff --git a/chap-17-go modules/imgs/1_modules.33fb029e.jpg b/chap-17-go modules/imgs/1_modules.33fb029e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c183f05d9caed9d7b2cda1d4ce237a636104c1df Binary files /dev/null and b/chap-17-go modules/imgs/1_modules.33fb029e.jpg differ diff --git a/chap-17-go modules/imgs/3_modules.3b193265.png b/chap-17-go modules/imgs/3_modules.3b193265.png new file mode 100644 index 0000000000000000000000000000000000000000..ab70780c9e97fa61a9bffd496ff1cea40eaca11e Binary files /dev/null and b/chap-17-go modules/imgs/3_modules.3b193265.png differ diff --git a/chap-17-go modules/imgs/dep_graph.627c0be1.png b/chap-17-go modules/imgs/dep_graph.627c0be1.png new file mode 100644 index 0000000000000000000000000000000000000000..5e8ccdfbb72dfee86ac2495403aa05c72200a3ff Binary files /dev/null and b/chap-17-go modules/imgs/dep_graph.627c0be1.png differ diff --git a/chap-17-go modules/imgs/dep_graph_version.7b4e7e6f.png b/chap-17-go modules/imgs/dep_graph_version.7b4e7e6f.png new file mode 100644 index 0000000000000000000000000000000000000000..ef71843e349ae3d6abfa169870310bf5e78155db Binary files /dev/null and b/chap-17-go modules/imgs/dep_graph_version.7b4e7e6f.png differ diff --git a/chap-17-go modules/imgs/gitlab_new_tags.cc2ffc37.png b/chap-17-go modules/imgs/gitlab_new_tags.cc2ffc37.png new file mode 100644 index 0000000000000000000000000000000000000000..5c9ae333fdbb21cc8f2ecd6270f4bb0f347b8b0c Binary files /dev/null and b/chap-17-go modules/imgs/gitlab_new_tags.cc2ffc37.png differ diff --git a/chap-17-go modules/imgs/go_mod_graph.eeda79c9.png b/chap-17-go modules/imgs/go_mod_graph.eeda79c9.png new file mode 100644 index 0000000000000000000000000000000000000000..f043f44990dce780bc53aff90a13ec32cae92920 Binary files /dev/null and b/chap-17-go modules/imgs/go_mod_graph.eeda79c9.png differ diff --git a/chap-17-go modules/imgs/go_sum.37f26b1e.png b/chap-17-go modules/imgs/go_sum.37f26b1e.png new file mode 100644 index 0000000000000000000000000000000000000000..51abe68e87f9494ce29bd7241295d41c8af2a14c Binary files /dev/null and b/chap-17-go modules/imgs/go_sum.37f26b1e.png differ diff --git a/chap-17-go modules/imgs/modules_requirement_mindmap.83a56a6e.png b/chap-17-go modules/imgs/modules_requirement_mindmap.83a56a6e.png new file mode 100644 index 0000000000000000000000000000000000000000..3964a163e51c703c87a43429428b004b983297c4 Binary files /dev/null and b/chap-17-go modules/imgs/modules_requirement_mindmap.83a56a6e.png differ diff --git a/chap-17-go modules/imgs/tree_cache_loir402.d8a3d2d0.png b/chap-17-go modules/imgs/tree_cache_loir402.d8a3d2d0.png new file mode 100644 index 0000000000000000000000000000000000000000..1d955879c663c75eecc959a97683b73176f5afc5 Binary files /dev/null and b/chap-17-go modules/imgs/tree_cache_loir402.d8a3d2d0.png differ diff --git a/chap-17-go modules/imgs/tree_cendor_go_module.65092c6c.png b/chap-17-go modules/imgs/tree_cendor_go_module.65092c6c.png new file mode 100644 index 0000000000000000000000000000000000000000..2273b180177947c55f5094f1656a4c1892c57217 Binary files /dev/null and b/chap-17-go modules/imgs/tree_cendor_go_module.65092c6c.png differ diff --git a/chap-17-go modules/imgs/tree_view_chache_download.64987ec9.png b/chap-17-go modules/imgs/tree_view_chache_download.64987ec9.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3d4d95d13bb68fad893a07a0396f475f59d104 Binary files /dev/null and b/chap-17-go modules/imgs/tree_view_chache_download.64987ec9.png differ diff --git a/chap-17-go modules/imgs/tree_view_package_timer.7a58fc4c.png b/chap-17-go modules/imgs/tree_view_package_timer.7a58fc4c.png new file mode 100644 index 0000000000000000000000000000000000000000..ce461ad573acb3133c9d64b9f31c7faa501efbee Binary files /dev/null and b/chap-17-go modules/imgs/tree_view_package_timer.7a58fc4c.png differ diff --git a/chap-17-go modules/imgs/upgrade_one_module_to_specific_newer.dab029d9.png b/chap-17-go modules/imgs/upgrade_one_module_to_specific_newer.dab029d9.png new file mode 100644 index 0000000000000000000000000000000000000000..73125cc5b8df7815f1c58148b84984559bc81979 Binary files /dev/null and b/chap-17-go modules/imgs/upgrade_one_module_to_specific_newer.dab029d9.png differ