从Go语言诞生并投入生产使用以后,对Go中及其不完善的依赖库管理功能的骂声就没有停止过。这种现象直到Go 1.11版本引入了Go Module,并在1.14版本转入生产使用以后,才逐渐好转。但是从1.0版本开始一直到1.14版本已经经过了不短的时间,有相当一部分的库都没有采用能够支持Go Module的方式进行管理,这也让Go Module的使用显得有些尴尬。总起来说,这种情况的存在并不会影响Go Module对于Go语言应用依赖库管理方法的改善。
使用Go Module功能来管理Go程序依赖库的核心,就是在go
命令中引入的go mod
子命令。在新版本的Go语言中,一个新应用就是从go mod
命令开始的。
混沌时代
Go语言的混沌时代是Go 1.11引入Go Module之前的版本。叫它混沌时代的意思就是,在这个时期开发一个Go应用是存在很大的不确定性的。
熟悉Go编程的人都知道,Go干的野心最大的一件事就是把整个Github作为了它的依赖库托管。这种设计节省了建立Go依赖库的成本,但是又因为Git本身的设计,让Go只能从Github上获取依赖库内容,但是不能明确的获取依赖库的某一个版本。所以早期的Go语言在获取依赖的时候,都是直接使用go get
命令直接获取目标依赖库的最新代码。
如果你没有经历过这个时代,你可以尝试想象一下,这种依赖库的获取行为会出现什么现象。
独自开发一个应用
当你独自开发一个应用的时候,你不会碰到任何问题。无论是go get
还是go get -u
命令都可以很好的在你的及其上运行,你正在开发的应用也不会出现任何混乱。如果你长时间都是这种工作状态,你应该不会体会到Go Module带来的差别。
在这个时期,
go get
会拉取指定依赖库的最新代码,如果本地已经存在了指定依赖库的缓存,那么就不会再拉取了,而go get -u
会在本地存在指定依赖库缓存的时候更新拉取到本地的依赖库代码。
独自开发两个应用
当你再构建一个新的应用的时候,令你头大的事情就要发生了。如果你的两个应用都使用了同一个依赖库,那你再使用go get -u
命令的时候就要十分的小心了。
对于同一个依赖库,Go会在你本地保存一份缓存,你正在开发的两个应用都会依赖于这一份缓存。如果你执行了go get -u
命令,那么这一份缓存将会被更新,然后,你的两个应用就都会开始使用最新的依赖库代码了。一个依赖库的版本库中如果存在新的提交,那么说明这个依赖库的代码已经进行了修改,如果只是简单的Bug修复还好,如果这个依赖库进行了断代式升级呢?
在执行完go get -u
命令以后,你会有很大的概率发现你的两个应用都不能正常编译或者运行了,现在你要面临的事情,就是修改你的应用,让它们能够使用新版本的依赖库工作。
大部分Go依赖库提供的依赖库安装命令都是采用
go get -u
的格式,其实他们的目的只是推荐你使用库的最新版本,但是实际使用起来,你懂的……
那扩展一下……如果你正在维护好几个应用呢?
大家一起开发一个应用
当大家一起开发一个应用的时候,go get
命令带来的不和谐效果就扩大了。
比如李一一建立了一个项目,然后在自己的机器上安装了一套项目所需要使用的依赖库的缓存,然后把项目代码放到了Git上供团队中其他的人合作使用。然后钱二二拉取了项目的代码,也在自己的机器上安装了项目所使用的依赖库的缓存。团队里两个人所使用的依赖库缓存都是同一个版本的,所以一切看起来都很美好,很和谐。
有一天,有一个新成员程三三加入了进来,他要做的事情同样是拉取项目的代码,然后在自己的机器上安装项目所使用的依赖库的缓存。但是这会儿不和谐的事情出现了,在程三三加入到团队之前,依赖库的版本已经更新了,但是团队并没有跟进,依旧使用的旧版的依赖库代码。程三三在自己的机器上运行了go get -u
命令,结果就是程三三的机器上的缓存是最新的依赖库代码,然后程三三发现项目在自己的机器上运行不起来。
现在好了,整个团队面临着一个选择,该选哪个呢。
- 大家都运行
go get -u
命令,升级自己机器上的依赖库缓存,然后修改项目代码,让项目能够使用新版本的依赖库工作。 - 程三三去各位前辈的机器上手工复制依赖库缓存,然后放到自己机器上。
- 把依赖库缓存也作为项目的一部分放到版本库里,保证大家都用一样的缓存。
如果选第一个,那么基本上就相当于给项目做了一次重构。但是一个项目所依赖的库不可能只有一个,所以任何一个团队都不会轻易选择这一项,让重构的工作占掉自己的大部分时间。如果选第二个,那么程三三必须对Go的缓存非常了解,而且可能还需要一个比较大的U盘。第三个选择看起来比较合适,但是同样也要求使用者对Go的缓存比较了解,而且版本库的空间使用也是一个问题。
大家一起开发两个应用
项目越多越混乱,团队人越多也越混乱,两个条件加在一起,大家的钱包可能都要瘪一瘪,体重都要长一长了。
一切都是版本惹的祸
其实Go把Github当作依赖库托管的方法没错,但是从上面这些混乱,可以看出来一点,导致这些混乱的根本原因就是Go没办法冻结项目所使用依赖库的版本。如果能冻结项目所使用的依赖库版本,那这一切问题都解决了。所以,要解决的问题就是,怎么在Git上加上依赖库的版本描述呢?
Go Module的引入,可以算是非常有效的解决了这个问题。
配置Go Module
安装Go 1.14版本以后,Go Module就已经自动启用了,不必再按照往上其他教程中所描述的那样需要去调整GO111MODULE
环境变量的值了。除非你安装的是Go 1.11版本,需要通过环境变量来打开Go Module的支持,不过在最新版已经是1.19的今天,你应该不会去安装旧版Go语言的。
现在完成Go语言的安装以后你需要配置的环境变量主要是以下几个。
GOROOT
用于指定Go在机器上的安装位置。GOPATH
默认情况下与GOROOT
采用相同的值,不过从Go 1.1版本开始强制要求改为与GOROOT
不同的路径,主要用于存放源码文件、可执行文件和二进制库文件。GOARCH
用于指定编译目标机器的处理器架构,常用的值为386
(x86架构)、amd64
(x64架构)、arm
等。GOOS
用于指定编译目标机器所使用的操作系统,常用的值为linux
、windows
、darwin
(macOS)和freebsd
等。GOARM
用于指定基于ARM架构的处理器版本。GOPROXY
用于指定Go Module获取依赖库时所使用的代理服务器。
GOBIN
,这个环境变量用于指定编译器和连接器的安装位置,对于Go 1.0.3以后的版本,编译器和连接器都已经集成在了Go中,也就是在$GOROOT/bin
中,所以就无需再配置GOBIN
了。
GOARCH
、GOOS
和GOARM
这几个用于指定编译目标机器配置的环境变量并不需要固定到系统的环境变量中,这几个会影响Go程序编译的环境变量更多的是使用在交叉编译中。而交叉编译除了可以通过设置环境变量来编译,还可以借助目前的容器化技术来实现,所以对于Go所支持的环境变量,配置的越少越好。
身在中国的你需要知道的
Go并不是只能使用Github作为其依赖库托管,而是所有基于Git的版本库系统都可以作为依赖库托管。所以在进行Go语言编程的时候,除了会使用到以github.com
开头的依赖库以外,还会使用到放置在其他Git版本库系统中的依赖库,例如gitee.com
等。
对于大部分的Git版本库系统,在国内还是可以正常访问的,但是最常用的一套依赖库就是一个例外:golang.org/x
。这套依赖库是Go标准库的一个补充,有很多没有被包含在标准库中的功能,大多都在这一套依赖库中提供了支持。但是这一套依赖库是托管在golang.org
上的,所以,你懂的……
在没有GOPROXY
环境变量的时候,想在项目中使用golang.org/x
依赖库,需要花费很多的心思,甚至有大神提供了使用Github上的源码来替代golang.org
上的托管的方法。
GOPROXY
环境变量实际上是一个列表,Go在编译的时候可以利用其定义的代理服务器获取程序所使用的依赖库。虽然可以在GOPROXY
中指定使用多个代理服务器,但是没有必要,容易造成编译失败或者编译缓慢的情况。在实际使用中,只需要选择以下代理服务中的一个使用即可。
https://goproxy.cn
,七牛云提供的代理服务。https://mirrors.aliyun.com/goproxy/
,阿里云提供的代理服务。https://proxy.golang.com.cn
,goproxy.io提供的代理服务。
设置代理服务器,只需要将环境变量的值编辑成这样即可:GOPROXY=https://goproxy.cn,direct
,其中的direct
表示在代理服务不可用的时候的降级策略,也即直连。
对于一些不必要使用代理的托管服务,可以通过配置GOPPRIVATE
环境变量来使其绕过GOPROXY
的代理。GOPRIVATE
环境变量是为私有库准备的,它的值同样也是一个逗号分隔的列表,其中只需要书写需要绕过代理的托管服务域名即可,例如GOPRIVATE=.gitee.com,.gitlab.com
。
GOPRIVATE
中所设置的域名前面都有一个.
,而不仅仅只是一个域名。
同样的,在运行文章后面所提到的go mod vendor
命令的时候,经常也会被报出i/o timeout
的错误,这是因为Go在验证依赖包完整性所使用的网站被屏蔽的结果,所以还需要通过GOSUMDB
环境变量指定一个可以使用的验证服务。这个验证服务可以选择以下选项中的一个。
sum.golang.google.cn
,这事Google专为国内环境提供的验证服务。gosum.io
,由goproxy.io提供的验证服务,其服务具体链接需要通过网站获取。off
,这个选项会关闭依赖包的验证。
创建新应用
在引入Go Module之前的Go版本中,创建一个新项目只需要在$GOPATH/src
中创建一个新的目录即可,而这个项目所依赖的其他库也都同样的放置在这个目录下,而且如果你的项目是一个库,那么可能你还需要按照github.com/account/project-name
的目录结构格式放置你的项目,这样才能正确的发布到Github上然后再被其他的人正确的引用。
但是在引入Go Module之后,一切就变得不一样了。首先新的项目不能在$GOPATH/src
目录中创建了,而是要在$GOPATH
以外的目录中创建,而且新的项目可以使用go mod
命令创建了。
go mod
命令虽然作为go
命令的子命令,但是它还是提供了更进一级的一系列子命令来支持Go Module功能的使用。跟其他语言的创建新项目命令比起来,go mod
创建新项目的命令显得特别的随意。使用go mod
创建新项目需要首先手动创建一个用于存放项目的目录,然后在这个目录中执行命令go mod init <项目模块名>
。
go mod init
所需要的项目模块名实际上就是这个项目的Module名称,也是其他应用连接和引入(import
)这个项目所使用的模块名。这个模块名可以与目录的名称不同。
执行go mod init
命令以后,在这个项目的根目录中就会自动的创建一个名为go.mod
的文件,这个文件里的内容在最开始创建的时候只有两行。
|
|
这个文件的初始内容主要声明了项目的基础模块名和所依赖的Go语言版本。注意,go
关键字指定了编译这个包所需要的最低Go语言版本,如果在低版本的Go语言环境中引入了依赖于高版本Go语言环境的依赖库,那么就有可能无法成功编译,例如Go 1.18中新引入的泛型功能。
go.mod
文件的内容,这个文件一旦被创建以后,其内容就会被Go语言的工具链接管。
与go.mod
文件一同会存在的还有一个名为go.sum
的文件,这个文件通常在添加依赖库以后会出现,其中主要记录的是所添加的依赖库的验证签名,用于与前文所提到的验证数据库进行比对的。这个文件同样也不需要手动修改,在项目的依赖库发生变化的时候,这个文件会被自动更新。
go mod init
创建的新项目中,不包含任何的.go
代码文件,你需要自行建立,这也是Go语言的项目创建指令与其他语言不同的一个地方。
管理依赖库
go.mod
文件中主要是通过module
、require
、replace
和exclude
四个关键字来定义和控制项目中所使用的依赖库的。
module
关键字用于指定当前项目的包的名称以及引用路径。require
关键字用于指定编译项目所必须的依赖项模块。replace
关键字用于替换require
中的依赖项模块,例如将一个公开的托管在Github上的模块替换成一个经过修改的位于私有地址的模块。exclude
关键字用于从目前项目的关联依赖项中排除不需要的项目。
例如这是一个增加了fiber框架的go.mod
文件。
|
|
因为在这个项目中还没有真正的编写任何代码,所以所有的依赖库都被打上了// indirect
标记。但是还是可以看出来go.mod
文件中是如何定义项目所以来的依赖库版本的。每一条依赖库记录的格式都是依赖库路径 依赖库版本
,例如github.com/gofiber/fiber/v2 v2.38.1
,其意义就是fiber库在import
的时候需要使用github.com/gofiber/fiber/v2
路径来进行,项目所以来的fiber库版本是v2.38.1
。
在go.mod
中还经常可以看到类似于golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9
这样的依赖库定义,这条定义的版本号十分的繁琐,这是因为这种版本号是v0.0.0
的依赖库没有按照Go Module的规则来发布其版本,所以Go Module就使用v0.0.0-Git提交时间-Git提交Hash
的格式来确定所使用的依赖库版本在Git版本库中的位置。
Go对于各个Module所采用的版本号采用了语义化版本号规范。在这个规范下,版本号被分为了三个部分,格式为vX.Y.Z
,其中每一部分的含义如下。
X
为主版本号(Major Version),当应用做出了不兼容的API修改时,主版本号需要增加。Y
为次版本号(Minor Version),当应用做出了向下兼容的功能性增加或修改的时候,次版本号需要增加。Z
为修订版本号(Patch Version),当应用中做了向下兼容的缺陷修正,修订版本号需要增加。
这三个版本号都必须为0或正整数,且都不可以添加前导0,也不可以添加空格、标点等符号。
不过在这种简单语义化版本号的基础上还存在一种附加了更多内容的版本号形式,格式为vX.Y.Z-R+B
,其中R
为先行版本号(Pre-release),用来在修订版本号相同的情况下区分不同的发布,B
为编译版本号,用来在修订版本号相同的情况下对编译次数进行计数。比较常见的实际版本号例如v1.0.4-alpha+054
,这代表应用的版本为1,没有新加功能,做了4个版本的缺陷修订,目前发布了Alpha版本,当前版本是第54次编译。语义化版本号规范并没有对先行版本号和编译版本号的内容组成做出严格的限制,但是在使用的时候,需要注意先行版本号前需要使用-
作为前缀,编译版本号前需要使用+
作为前缀,这两个附加内容中都不允许有空格出现,但内容中的多个部分可以使用.
分隔。
增加依赖库
向项目中增加一个依赖库和没有引入Go Module之前增加一个依赖库的方法是没有区别的,都是使用go get
命令。例如在之前的示例中增加fiber框架支持的命令就是go get github.com/gofiber/fiber/v2
,Go Module会自动将所需的依赖以及间接依赖都加入到go.mod
文件的require
关键字中。
go get
向go.mod
中加入了新的依赖但是还没有在项目中真正的引入使用,那么就不要使用go mod tidy
命令对go.mod
文件内容进行清理。
$GOPATH/pkg
目录下面,所有的缓存都会携带有版本号。
更新依赖库
要更新一个依赖库的版本并不需要手动去编辑go.mod
文件,而是可以直接借助之前所提到过的go get -u
命令。在项目中执行不同格式的go get -u
命令会有不同的效果。
- 执行
go get -u
命令将会使依赖库升级到最新的次版本号或者修订版本。 - 执行
go get -u=patch
命令将会使依赖库升级到最新的修订版本。 - 执行
go get package@version
将会使依赖库升级到指定的版本号。 - 直接执行
go get
将会自动检查指定依赖库的版本变化,如果存在变化,那么命令将会自动修改go.mod
。
如果不确定当前项目中有哪些依赖库可以升级,可以执行命令go list -m -u all
来进行检查,如果需要升级所有可以升级的依赖库,可以执行命令go get -u
这个不附带任何指定依赖库的命令版本来升级。
替换依赖库
在实际项目开发的过程中,不是所有的依赖库都可以顺利的下载到本地,例如之前示例中存在的golang.org/x/sys
,在下载的时候就可能会失败。如果没有配置GOPROXY
或者代理服务失效,那么就需要使用之前提到过的替换法来从另一个托管位置下载。在有了Go Module以后,这个替换就方便的多了。
比如golang.org/x
系列的module经常会使用其在github.com
上的镜像替换,这时就要在go.mod
文件中使用到replace
关键字了。replace
关键字一般用在require
列表的后面,例如将golang.org/x/sys
进行一个替换,内容就是以下这样的。
|
|
go mod
命令会直接修改replace
中的内容,你可以使用go mod edit
命令启动go.mod
文件的手动修改。
清理没有使用的依赖库
随着项目开发的进行和所使用依赖库的升级,go.mod
中渐渐的会有很多依赖库就会不再使用了,这些多余的依赖库在编译的时候会增加编译时间,提升不必要的成本,所以在完成一定版本的项目开发以后,需要及时的对go.mod
文件进行清理。
清理go.mod
文件也不需要手动进行,只需要执行go mod tidy
即可,go mod
会自动对项目中没有使用的依赖库声明进行清除。
缓存依赖库
将依赖库下载到本地有两种方式,第一种是直接下载到本地缓存,这主要是用于在本地建立一个已有项目的编译和运行环境。另一种是将依赖库下载到vendor
目录下。
在第一种情况下,可以直接在项目根目录中执行go mod download
命令,即可将go.mod
文件中声明的依赖库都下载到本地缓存中。这样就不必再使用go get
命令逐一进行依赖库的获取了。
对于第二种情况中出现的vendor
目录则需要做一些额外的说明。在Go Module被引入之前,Go 1.5版本引入了一种解决项目依赖保存的方法,就是在项目的目录中创建一个名为vendor
的目录用来保存项目中所使用到的依赖库。在引入vendor
目录以后,对于Go代码的编译就会优先从vendor
目录中查找依赖库,然后再从$GOPATH
中查找。
虽然已经引入了Go Module的功能,但是这种将所有依赖库都保存在vendor
目录中的方式依旧可以避免一些编译错误的出现,所以将依赖库复制到vendor
目录中的做法就被保留了下来。现在已经不需要在手动构建vendor
目录的内容了,只需要使用go mod vendor
命令即可把go.mod
文件中声明的依赖库都复制到vendor
目录中。
vendor
目录保存依赖库的时候需要注意,vendor
目录中保存的依赖库是没有保存依赖库的版本信息的,所以即便是有了一个独立于其他项目保存依赖库的位置,但是前面提到的依赖库版本混乱的情况依旧存在。
发布自己的依赖库
将自己的项目发布为依赖库需要遵循Go对于依赖库的两条要求。
- 遵守依赖库的兼容性规则。
- 遵守语义化版本号规范。
依赖库兼容性规则
其实Go对于依赖库的兼容性要求十分简单,从核心来说就以下两条。
- 如果新库与旧库使用了相同的导入路径(即
module
名称),那么新库与旧库之间必须是兼容的。 - 如果新库不能兼容旧库,那么新库必须更换导入路径。
根据Google的推荐,为新库更换导入路径并不是非常麻烦的事情,对于发生了不兼容变更的依赖库来说,其主版本号必定要增加,所以Go Module规定,如果Module的主版本号大于1,那么需要在module名称中显式的增加版本号标识,例如依赖库v1.0.0
的导入路径为github.com/author/lib
,那么v2.0.0
的导入路径就为github.com/author/lib/v2
。遵循这样的规范,Go会将这两个不同版本的依赖库视为两个Module,从而避免可能存在的冲突问题。
发布带有语义化版本号的库
Git版本库的主要功能还是对文件的版本进行管理,并不是专为依赖库的托管设计的。所以Go Module为了能够顺利在Git版本库中对所依赖的依赖库版本进行定位,就同时对如何使用Git也做出了一些要求。在Git中对于依赖库的版本控制是通过tag
实现的,如果需要发布一个依赖库的新版本,那么只需要建立一个tag
,然后再推送到Git托管服务就可以了。
建立这种发布版本的命令可以使用以下两条。
|
|
如果需要对这个发布的版本进行修复,不要在master
分支上进行修复,然后再创建一个新的标签,可以使用以下命令从master
中创建一个分支进行修复。
|
|
module
中的导入路径。