Go Moudle笔记

1、go构建模式的演化

  go程序是由一系列的GO包组合而成,Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程。
  Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH、1.5 版本的 Vendor 机制,以及现在的 Go Module。

1、GOPATH

  GOPATH是go在最初开源的时候内置的一种构建模式。在这种构建模式下,GO编译器可以在本地的GOPATH环境变量配置的路径下,搜寻GO程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误。
  GOPATH构建模式下,如果我们的GO程序要导入一个github.com/user/repo包,我们定义的GOPATH目录是

GOPATH=E:\GoCode

那么在 GOPATH 构建模式下,Go 编译器在编译 Go 程序时,就会在下面这个路径下搜索第三方依赖包是否存在:

E:/GoCode/github.com/user/repo

此时我们可以通过go get命令将本地缺失的第三方依赖包下载本地,比如

go get github.com/sirupsen/logrus

这里的 go get 命令,不仅能将 logrus 包下载到 GOPATH 环境变量配置的目录下,它还会检查 logrus 的依赖包在本地是否存在,如果不存在,go get 也会一并将它们下载到本地。
  但是这种构建方式会产生很大的问题,go get 下载的包只是那个时刻各个依赖包的最新主线版本,这样会给后续 Go 程序的构建带来一些问题。比如,依赖包持续演进,可能会导致不同开发者在不同时间获取和编译同一个 Go 包时,得到不同的结果,也就是不能保证可重现的构建(Reproduceable Build)。又比如,如果依赖包引入了不兼容代码,程序将无法通过编译。也就是说,在 GOPATH 构建模式下,Go 编译器实质上并没有关注 Go 项目所依赖的第三方包的版本。

2、vender机制

  Go在1.5版本引入了vendor机制,其本质就是在Go项目下的某个特定目录下面将所有依赖包缓存起来,这个特定的目录名就是vendor。

.
├── main.go
└── vendor/
    ├── github.com/
    │   └── sirupsen/
    │       └── logrus/
    └── golang.org/
        └── x/
            └── sys/
                └── unix/

  Go 编译器会优先感知和使用 vendor 目录下缓存的第三方包版本,而不是 GOPATH 环境变量所配置的路径下的第三方包版本。这样,无论第三方依赖包自己如何变化,无论 GOPATH 环境变量所配置的路径下的第三方包是否存在、版本是什么,都不会影响到 Go 程序的构建。
  要想开启 vendor 机制,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。如果不满足这一路径要求,那么 Go 编译器是不会理会 Go 项目目录下的 vendor 目录的。
  虽然vendor解决了初始GOPATH的问题,但是庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度,而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。

3、Go Module

  从 Go 1.11 版本开始,除了 GOPATH 构建模式外,Go 又增加了一种 Go Module 构建模式。
  一个 Go Module 是一个 Go 包的集合。module 是有版本的,所以 module 下的包也就有了版本属性。这个 module 与这些包会组成一个独立的版本单元,它们一起打版本、发布和分发。在 Go Module 模式下,通常一个代码仓库对应一个 Go Module。一个 Go Module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module,也就是说 Go Module 与 go.mod 是一一对应的。go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module。
我们可以通过如下步骤去基于当前仙姑去创建一个 Go Module

第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
  go mod init 在当前项目目录下创建了一个 go.mod 文件,这个 go.mod 文件将当前项目变为了一个 Go Module,项目根目录变成了 module 根目录。
第二步,通过·go mod tidy 命令自动更新当前 module 的依赖信息;
  go mod tidy 命令会添加 module 依赖以及校验和。go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及版本,下载这些依赖并更新本地的 go.mod 文件。
  而且,执行完 go mod tidy 后,当前项目除了 go.mod 文件外,还多了一个新文件 go.sum。这是由 go mod 相关命令维护的一个文件,它存放了特定版本 module 内容的哈希值。这是 Go Module 的一个安全措施。当将来这里的某个 module 的特定版本被再次下载的时候,go 命令会使用 go.sum 文件中对应的哈希值,和新下载的内容的哈希值进行比对,只有哈希值比对一致才是合法的,这样可以确保你的项目所依赖的 module 内容,不会被恶意或意外篡改。因此,我推荐你把 go.mod 和 go.sum 两个文件与源码,一并提交到代码版本控制服务器上。
Go Moudle笔记_第1张图片

第三步,执行 go build,执行新 module 的构建。、

2、深入 Go Module 构建模式

  随着项目规模的增大,项目所依赖的包会逐渐边的更复杂。并且项目所依赖的包有很多版本,那么Go Module 是如何选出最适合的那个版本的呢?
  Go 语言设计者在设计 Go Module 构建模式,来解决“包依赖管理”的问题时,进行了几项创新,这其中就包括语义导入版本 (Semantic Import Versioning),以及和其他主流语言不同的最小版本选择 (Minimal Version Selection) 等机制。

Go Module 的语义导入版本机制

  通过上图可以看到go.mod 的 require 段中依赖的版本号,都符合 vX.Y.Z 的格式。语义版本号分成 3 部分:主版本号 (major)、次版本号 (minor) 和补丁版本号 (patch)。
Go Moudle笔记_第2张图片
按照语义版本规范,主版本号不同的两个版本是相互不兼容的。而且,在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。
  而且,Go Module 规定:如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。怎么理解呢?我们来举个简单示例。我们就以 logrus 为例,它有很多发布版本,我们从中选出两个版本 v1.7.0 和 v1.8.1.。按照上面的语义版本规则,这两个版本的主版本号相同,新版本 v1.8.1 是兼容老版本 v1.7.0 的。那么,我们就可以知道,如果一个项目依赖 logrus,无论它使用的是 v1.7.0 版本还是 v1.8.1 版本,它都可以使用下面的包导入语句导入 logrus 包:

import "github.com/sirupsen/logrus"

  那么问题又来了,假如在未来的某一天,logrus 的作者发布了 logrus v2.0.0 版本。那么根据语义版本规则,该版本的主版本号为 2,已经与 v1.7.0、v1.8.1 的主版本号不同了,那么 v2.0.0 与 v1.7.0、v1.8.1 就是不兼容的包版本。然后我们再按照 Go Module 的规定,如果一个项目依赖 logrus v2.0.0 版本,那么它的包导入路径就不能再与上面的导入方式相同了。那我们应该使用什么方式导入 logrus v2.0.0 版本呢?

  Go Module 创新性地给出了一个方法:将包主版本号引入到包导入路径中,我们可以像下面这样导入 logrus v2.0.0 版本依赖包:

import "github.com/sirupsen/logrus/v2"

这就是 Go 的“语义导入版本”机制,也就是说通过在包导入路径中引入主版本号的方式,来区别同一个包的不兼容版本,这样一来我们甚至可以同时依赖一个包的两个不兼容版本:

import (
    "github.com/sirupsen/logrus"
    logv2 "github.com/sirupsen/logrus/v2"
)

Go Module 的最小版本选择原则。

最小版本选择原则很简单,与其他包依赖管理工具都会选择依赖项的“最新最大 (Latest Greatest) 版本”不同,Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。

3、Go Module常规操作

1、为当前项目添加一个依赖包

首先要更新源码,在源码中添加需要的依赖

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

后面我们可以通过两种方式添加依赖
1、我们可以通过手动go get命令将我们新增的依赖包下载到了本地 module 缓存里,此时go.mod 文件的 require 段中新增了一行内容:

require (
	github.com/gin-gonic/gin v1.8.1 // indirect
)

2、我们也可以使用 go mod tidy 命令,在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载它们

2、升级 / 降级依赖的版本

  我们以上面的logrus为例,logrus现在就存在着多个发布版本,可以用下面的命令查询

$go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

  如果我们想将 logrus 版本降至某个之前发布的兼容版本,比如 v1.7.0,那么我们可以在项目的 module 根目录下,执行带有版本号的 go get 命令:

$go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0

从这个执行输出的结果,我们可以看到,go get 命令下载了 logrus v1.7.0 版本,并将 go.mod 中对 logrus 的依赖版本从 v1.8.1 降至 v1.7.0。
  当然我们也可以使用万能命令 go mod tidy 来帮助我们降级,但前提是首先要用 go mod edit 命令,明确告知我们要依赖 v1.7.0 版本,而不是 v1.8.1,这个执行步骤是这样的:

$go mod edit -require=github.com/sirupsen/logrus@v1.7.0
$go mod tidy       
go: downloading github.com/sirupsen/logrus v1.7.0

对于升级的步骤与降级的步骤相同。
  在前面的例子中,Go Module 的依赖的主版本号都是 1。根据我们上节课中学习的语义导入版本的规范,在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,不需要在包的导入路径上增加版本号,也就是:

import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo

3、添加一个主版本号大于 1 的依赖

  语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。
  按照语义版本规范,如果我们要为项目引入主版本号大于 1 的依赖,比如 v2.0.0,那么由于这个版本与 v1、v0 开头的包版本都不兼容,我们在导入 v2.0.0 包时,不能再直接使用 github.com/user/repo,而要使用像下面代码中那样不同的包导入路径,即在声明导入路径的基础上,加上版本号信息:

import github.com/user/repo/v2/xxx

4、升级依赖版本到一个不兼容版本

  我们前面说了,按照语义导入版本的原则,不同主版本的包的导入路径是不同的。所以,同样地,我们这里也需要先将代码中包导入路径中的版本号改为需要的版本 v*
  接下来通过go get来获取对新版本包的依赖。

5、移除一个依赖

  要想删除一个依赖,我们需要先在源码中删除对依赖项的导入语句,然后利用go mod tidy命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。

6、特殊情况:使用 vendor

你可能会感到有点奇怪,为什么 Go Module 的维护,还有要用 vendor 的情况?
  其实,vendor 机制虽然诞生于 GOPATH 构建模式主导的年代,但在 Go Module 构建模式下,它依旧被保留了下来,并且成为了 Go Module 构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境(CI/CD)中,使用 vendor 机制可以实现与 Go Module 等价的构建。
  和 GOPATH 构建模式不同,Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:

$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│   ├── google/
│   ├── magefile/
│   └── sirupsen/
├── golang.org/
│   └── x/
└── modules.txt

  我们看到,go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。
  如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 -mod=vendor 参数。
  在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入 -mod=mod 的参数。

你可能感兴趣的:(Go系列,golang)