我们要理解清楚这个问题,首先需要知道Go为了解决这个问题做了工作,遇到了哪些困难,做了哪些努力和改进。 了解了这些,我们就可以更加清楚这个问题。
1、Go构建模式是怎么演化的:做了哪些工作
- 早期的GOPATH
- 1.5版本后的vendor
- Go Module
1.1、GOPATH
在GOPATH模式下面,编译器可以在配置的GOPATH环境变量的目录下面,搜寻程序所需要的依赖包。
找的到就编译,找不到就报错。
如果没有设置GOPATH这个环境变量。
则GOPATH默认值是$HOME\go。
举例: 你用到了第三方依赖包:
package main
import "github.com/sirupsen/logrus"
func main() {
logrus.Println("hello, gopath mode")
}
假设,你并未下载这个github.com/sirupsen/logrus 这个依赖包。
go编译器先去你本地的go安装目录src目录找,如果没找到:
再去GOPATH配置的目录下面的src目录下面去找。
如果都没有,我们需要自己去下载一下,使用go get命令去下载。
这个命令会将第三方依赖下载至GOPATH配置的目录下面。
包括依赖的依赖,它也会去检测,如果不存在的话,也会去下载。
这个模式存在哪些问题?
很明显,就是版本的控制问题。
go get命令会拉取当前时刻,第三方依赖最新的版本。
通俗一点就是,不同的开发者,在不同时间下载第三方依赖的时候,会因为第三方依赖的偏差,导致编译出来的结果不一致。
甚至有可能,你需要的依赖包本身所依赖的包,由于作者的更新导致不可用。这种不可用会传递你的项目中。
1.2、vendor
为了解决上面的问题,引入了vendor的概念。
我认为vendor的概念非常好理解:
就是为了避免上面的版本不可控,引入的一种版本隔离。
具体实现简单介绍一下:
go开发者开发项目的时候,把依赖全部下载到vendor目录下面。以前是直接在GOPATH下面。
现在在vendor目录下面也优先存一份。
这个项目分享给其他人的时候,其他人进行编译,go编译器会自动优先从vendor目录下面获取依赖。
这样就实现了一个:可重现构建的功能。
所以,我们如果使用了vendoe,在提交代码的时候,也需要将vendor目录进行提交。
这就带了一个问题:非常占用空间,而且需要手工去管理vendor下面的依赖包,非常耗费心智。
这里需要有一点注意:要想让go编译器优先从vendor里面取,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面
1.3、Go Module
Go的1.11版本,带来了Go Module,与vendor诞生的理由差不多,为解决以前的痛点而诞生的。
Go Module 的三板斧:
- go mod init 会生成一个go.mod ,将项目变成Go Module
- go mod tidy 根据项目所需要的依赖,自动下载依赖,一些不需要的依赖,也会从项目中删除
- go build 对Module进行构建
与前面两种不同:Go Module 项目,不必在GOPATH下面!
Go Module 还支持通过 Go Module 代理服务加速第三方依赖的下载。
2、深入Go Module构建模式
Go Module依赖版本去管理,也就需要一定的规矩。目前的规矩如下:
- 主版本号相同,则兼容;反之则认为不兼容
- 主版本号相同,导入的路径也是一样的
举例: v1.7.0、v1.8.1 是互相兼容的版本。
v2.0.0 与上面版本不兼容。
v1.7.0、v1.8.1 在程序中导入是这样的:
import "github.com/sirupsen/logrus/v2"
而v2.0.0在程序中导入是这样的:
import "github.com/sirupsen/logrus/v2"
Go Module 的最小版本选择原则:
go编译器在选择c依赖的时候会选择哪一个呢?
按照上面规则,v1.1.0、v1.3.0、v1.7.0的主版本号相同,应该是互相兼容的,所以选择哪一个都行。
Go设计者认为只要满足整体需求的“最小版本”即可。所以选择v1.3.0版本。
下面这个图需要牢记一下,万一碰到以前的历史项目,可以参考一下。
如果都是新项目,就不必要记录,因为默认是开了Go Module的