10分钟带你了解Go依赖管理常见模式(一)

1,758 阅读10分钟

我们在编写程序的时候,或多或少都会引用第三方库,借助第三方库,我们可以更方便、更快地完成功能需求的实现。即便不引用第三方库,我们也会使用到团队内部其他开发人员所写的库。

因为需要引用别人写好的库,我们就会涉及到依赖包的管理,包括依赖包的下载、更新、版本管理等等。

我们当然可以手动对依赖包进行管理,但这样的效率会特别低,还涉及到编译器能否识别的问题。所以,一般情况下,编程语言都会有自己的管理模式,像大家很熟悉的 Java 语言,最常见的就是通过 maven 工具来完成包管理、程序构建、程序打包等工作。

那么,Go 语言又是怎么解决依赖包管理这个问题的?下面就一起来看看 Go 语言构建模式的演化。由于内容较多,将会拆分成两篇文章来输出,下图是对所有内容的归纳。

image.png

一、理解什么是"程序的构建"

前面我们说我们要来探讨 Go 语言构建模式的演化,那么我们首先要理解一个概念,什么是 Go 程序的构建?

程序的构建,指的就是当我们把代码写好之后,对代码进行编译打包的过程。

在这个过程中,Go 编译器会将代码、依赖包、配置文件等相关的东西都进行打包、编译、链接,最终形成一个可执行文件。而这个过程,需要有 Go 编译器和依赖管理工具的支撑。

而 Go 语言的构建模式,指的就是 Go 编译器完成构建过程所依赖的机制。

Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH 模式、1.5 版本的 Vendor 模式,以及现在的 Go Module 模式。

在这篇文章里,我们来介绍前两个构建模式。

二、Go 构建模式始祖 GOPATH

我们先来说说 GOPATH 模式。

Go 语言在建立之初,就内置了一种叫 GOPATH 的构建模式。在 Go 的环境变量中,有一个名为 GOPATH 的变量,该变量配置了第三方依赖包的存放地址。在 GOPATH 模式下,Go 编译器在编译代码的时候,会在 GOPATH 变量所配置的路径下搜寻程序所依赖的第三方包,若找到,则使用搜寻到的包进行编译,否则,则编译报错。

2.1 GOPATH 模式编译构建初试

下面演示一下在 GOAPTH 模式下如何编译运行 Go 程序。因为我只有 Windows 系统,就只演示在 Windows 下如何操作,其他系统的操作都差不多,这里安装的 Go 版本为 1.18.1。

步骤一,修改 Go 环境变量 GO111MODULE 的值为 off,以关闭 Go Module 模式。 修改命令如下:

go env -w GO111MODULE=off

如果不关闭 Go Module 模式,那么 Go 编译器就会默认使用 Go Module 来构建程序。

步骤二,设置 Go 环境变量 GOPATH 的值,配置 GOPATH 的路径。  比如,我的系统里设置的 GOPATH 路径为:D:\Workspace\Go\gopath。

image.png

步骤三,在 GOPATH 路径下创建 src、bin、pkg 三个子目录。  目录结构如下:

D:\Workspace\Go\gopath
                          |—— bin
                          |—— pkg
                          |—— src

步骤四,在 src 目录下新建一个项目目录,项目名可以自己拟定,这里就叫"zapdemo"。  这个项目将会引入Uber开源的日志库 Zap,测试该日志库的使用。

步骤五,在项目根目录下新建一个 main.go 的源码文件。  此时的目录结构如下:

D:\Workspace\Go\gopath\src\zapdemo
                                          |—— main.go

源码文件的内容如下所示:

package main

import "go.uber.org/zap"

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    logger.Info("This is an INFO log")
}

步骤六,在命令行或终端中执行 go get 命令下载 zap 依赖包。  执行命令如下:

go get -u go.uber.org/zap

依赖包下载成功后,Go 编译器就会将 zap 包下载并安装到 GOPATH 所配置的路径下。

其中,zap 源码文件会被存放在 src 目录下,zap 归档文件会被存放在 pkg 目录下。此时大致的目录结构如下所示:

D:\Workspace\Go\gopath
                          |—— bin
                          |—— pkg
                                  |—— windows_amd64
                                          |—— go.uber.org
                                                  |—— zap.a
                          |—— src
                                  |—— go.uber.org
                                          |—— atomic
                                                  |—— ···
                                          |—— multierr
                                                  |—— ···
                                          |—— zap
                                                  |—— ···
                                  |—— zapdemo
                                          |—— main.go

步骤七,使用 go run 命令编译并运行 Go 程序。  程序运行后,就会在控制台打印相应的日志:

image.png

以上就是在 GOPATH 模式下编译运行 Go 程序的简单流程了。

2.2 再谈 GOPATH 的目录结构

我们再来看看 GOPATH 的目录结构是怎样的。

前面已经提过,在 GOPATH 路径下有 3 个子目录,分别是 bin、pkg、src。我们分别来看看这 3 个子目录的作用。

首先是 src 目录。

src 目录用于存放 Go 源码文件,包括我们自己编写的源码文件以及使用 go get 命令所下载的依赖包的源码文件都是存放在这里的。

这里需要特别注意,如果你所写的程序是要给别人去引用的,那么你的程序源码一定要放在 GOPATH 路径的 src 目录下,这样程序才能被 Go 编译器搜寻到,所以大家会看到这样的建议:在 GOPATH 模式下,Go 项目目录建议存放在 GOPATH 变量所配置的路径的 src 目录下。

再来看看 pkg 目录。

在前面,我们使用 go get 命令下载了 zap 库,Go 编译器除了将 zap 包下载下来之外,还会进行源码安装操作。

安装后如果产生归档文件,就会被存放在 pkg 目录下。

归档文件指的是特殊的 Go 静态库文件,以".a"为后缀名,在编译和链接程序时会被使用到。归档文件可以提高程序的构建速度。

归档文件的目录组织规则如下:$GOPATH/pkg/平台相关目录/包导入路径/归档文件

比如前面下载的 zap 包,它的归档文件路径就为"D:\Workspace\Go\gopath\pkg\windows_amd64\go.uber.org\zap.a"。

其中,windows_amd64为平台相关目录,由编译时的目标操作系统、下划线和目标CPU架构组成;go.uber.org为包导入路径,这个参考前面 main.go 源文件的 import 子句;zap.a为归档文件,归档文件名等于包名。

最后是 bin 目录。

我们在编译安装 Go 源码时,如果安装的是命令源码文件,就会生成一个可执行文件.

bin 目录就是用来存放安装时所生成的可执行文件的。

2.3 GOPATH 模式的不足

虽然基于 GOPATH 构建模式可以完成 Go 程序的编译和运行,但它却并不好用。这是为什么呢?因为 GOPATH 这种模式无法完全满足开发者的需求。

我们一起来看看 GOPATH 模式有什么不足之处。

大家想一想,前面我们使用 go get 命令下载 zap 依赖包的时候,有没有指定下载哪个版本的依赖包呢?并没有。

那 go get 命令会下载依赖包的哪个版本呢?答案是当前时刻依赖包的最新版本。

那你说我就是不想要最新版本的依赖包,想要次新版的,可不可以?这在 GOPATH 模式下是做不到的。GOPATH 模式并不支持下载指定版本号的依赖包。

那么问题就来了,当我们把写好的代码提交到代码仓库之后,别人下载我们的代码并进行编译运行时,同样需要使用 go get 命令去下载相关的依赖包,下载的依赖包版本就是别人执行 go get 命令时该依赖包的最新版本,而这个版本号又不一定跟我们编写代码时所采用的版本号一致,这样就可能出现不同的结果。

也就是说,GOPATH 构建模式无法实现 Reproduceable Build(可重现构建)

简单总结下,在 GOPATH 构建模式下,Go 编译器是不关注项目所依赖的第三方包的版本的,但我们在日常开发中又需要控制依赖包的版本,因此,GOPATH 构建模式存在一定的局限性。

既然 GOPATH 模式不能实现可重现构建,那么我们就来看看另一种构建模式 —— Vendor 机制是如何解决这个问题的。

三、可重现构建的 Vendor

Vendor 模式是在 Go 1.5 版本引入的,目的就是为了解决可重现构建的问题。

在 Vendor 模式下,程序项目的根目录下有一个名为 "vendor" 的目录,缓存了项目所依赖的所有第三方包,Go 编译器在编译代码时,会优先到 vendor 目录搜寻依赖包,这样一来,就算依赖包的版本有所变化,也不会影响项目的构建。

不过需要注意的是,在提交代码到代码仓库时,要把 vendor 目录一块提交,否则还是无法实现可重现构建。

3.1 Vendor 模式编译构建初试

还是一样来演示一下在 Vendor 模式下是如何编译运行 Go 程序的。

还是基于上面创建的 "zapdemo" 项目,按照以下的步骤进行环境的准备。

步骤一,在 zapdemo 目录下创建一个 vendor 目录。

步骤二,将下载到 src 目录下的 zap 源码拷贝到 vendor 目录下。  此时的目录结构如下:

D:\Workspace\Go\gopath\src
                                     |—— zapdemo
                                             |—— main.go
                                             |—— vendor
                                                     |—— go.uber.org
                                                             |—— atomic
                                                                     |—— ···
                                                             |—— multierr
                                                                     |—— ···
                                                             |—— zap
                                                                     |—— ···

步骤三,清空 pkg 目录下的内容,以防止对测试结果造成干扰。

步骤四,再次使用 go run 命令编译运行 main.go 源文件,观察是否正常运行。

image.png

可以看到,程序是可以正常运行的,Go 编译器也能正常寻找到 zap 依赖包。

但是这里需要注意一点,就是如果要 Vendor 机制生效,程序项目必须存放在 GOPATH 路径下的 src 目录中,否则 Go 编译器是不会理会 vendor 目录的存在的。这时就会出现下面的报错:

image.png

既然 Vendor 模式可以解决 Go 程序可重现构建的问题,那为什么 Vendor 模式还是比较少用呢?下面我们来看看 Vendor 模式又有什么不足之处。

3.2 Vendor 模式的不足

虽然说 Vendor 模式在一定程序上解决了可重现构建的问题,但其实在体验上却并不友好,这主要体现在以下两点:

  • vendor 目录下的依赖包需要开发者手动拷贝过去的,这就意味着开发者要手工执行依赖包获取、存放、版本管理等工作,这无疑降低了开发者的工作效率;
  • vendor 目录是需要上传到代码仓库中的,如果 vendor 目录占用的空间很大,不仅会占用代码仓库的存储空间,还会影响代码下载的速度。

为了解决以上问题,Go 社区也推出了一系列依赖包管理工具,比如:glide、dep等,但这些工具都或多或少存在一些使用上的问题。

所以,Vendor 模式也不是最好的依赖包管理机制。

好在 Go 官方从 Go 1.11 版本开始又新增了一种构建模式 —— Go Module。Go Module 是当下最推荐使用的构建模式。

关于 Go Module 相关的知识,我将在下一篇文章继续为大家介绍。

Reference

gk.link/a/11sHo

gk.link/a/11sHp

PDF 版本已备好,如有需要,可关注公众号【运维夜谈】,回复【构建模式】自取!

image.png