手把手教你如何创建及使用Go module
Go module是从Go 1.11版本才引入的新功能。其目标是取代旧的的基于GOPATH方法来指定在工程中使用哪些源文件或导入包。本文首先分析Go引入module之前管理依赖的优缺点,然后针对这些缺点,看module是如何解决的。一、传统的包管理方式-package在Go1.11之前,如果想要编写Go代码以及引入第三方包,则需要将源代码写在GOPATH/src目录下。即开发者只能将研发的项目放到GOPATH目录下。同时,将引入的第三方包会下载到GOPATH/pkg目录下。我们先来看下在这种包管理模式下,使用go get是如何安装依赖包的,然后再分析这种包管理的不足。
想要了解更多 Golang 相关的内容,欢迎扫描下方👇 关注 公众号,回复关键词 [实战群] ,就有机会进群和我们进行交流~
1.1 go get的工作流程
我们以在项目中引入github.com/go-redis/redis包为例。在项目中使用import导入该包:import "github.com/go-redis/redis"
然后我们需要使用go get命令将该包下载下来:go get github.com/go-redis/redis
运行go get命令后,Go会访问 https://github.com/go-redis/redis 并下载该包。一旦下载完成,该包就会被保存到 $GOPATH/pkg/github.com/go-redis/redis 目录下。那么从执行go get命令到包被保存到对应的目录期间,go get都经历了哪些过程呢?首先,Go会将包拼接成https协议的URL地址。这里是 https://github.com/go-redis/redis 。Go的第三方包是存储在像GIT或SVN这样的在线版本控制管理系统上的。Go目前支持的在线版本管理类型如下:Bazaar .bzr
Fossil .fossil
Git .git
Mercurial .hg
Subversion .svn
所以,在示例中,Go首先会解析github.com/go-redis/redis.git (模板格式:github.com/go-redis/redis{.type})。其次,根据支持的协议依次尝试clone该包。若该在线版本管理系统支持多种协议,那么Go会依次尝试。例如,Git支持 https:// 和 git+ssh:// 协议 , 那么Go会依次使用对应的协议进行解析该包。如果Go成功解析了对应的URL地址,那么该包将会被clone并保存到$GOPATH/pkg目录下。最后,若版本管理系统不是Go所支持的,则尝试查找META信息。在这种场景下,Go也会试图使用https或http协议拼装成的URL地址去解析。并从返回的HTML代码中查找META信息:"go-import" content="import-prefix type repo-root">
- import-prefix: 这是模块所导入的路径。在我们的示例中是github.com/go-redis/go
- type:在线版本管理系统的类型。可以是上面我们提到的Go支持的类型之一。在我们的示例中是git。
- repo-root: 代码仓库在版本控制系统中的根URL地址。例如,在我们的示例中,应该是 https://github.com/go-redis/redis.git。
二、现代包管理方式-module
2.1 什么是module
一个module就是一个包含多个package的目录,即一个package的集合。 其要实现的目标如下:- 首先,研发者应该能够在任何目录下工作,而不仅仅是在GOPATH指定的目录。
- 可以安装依赖包的指定版本,而不是只能从master分支安装最新的版本。
- 可以导入同一个依赖包的多个版本。当我们老项目使用老版本,新项目使用新版本时会非常有用。
- 要有一个能够罗列当前项目所依赖包的列表。这个的好处是当我们发布项目时不用同时发布所依赖的包。Go能够根据该文件自动下载对应的包。
- 一个module必须是一个代码控制系统的仓库,并且一个仓库应该只能包含一个module。
- 一个module应该包含一个或多个package。
- 一个包应该在同一个目录下包含一个或多个go文件
2.2 如何创建module
第一,我们在GOPATH之外的任何位置创建一个目录。这里我们使用encodex,该encodex包含一些对字符串的编码功能函数,例如md5,sha1等。如下图:根据上面所讨论的,一个Go module应该是一个版本控制系统上的代码仓库。所以我们在github上创建一个git的代码仓库,如下图:第二,在本地的目录下执行go mod init 命令来初始化Go module。
go mod init github.com/goxuetang/encodex
该命令会在encodex的根目录下创建go.mod文件,go.mod文件会包含我们定义的module的导入路径和依赖的包及对应的版本。如下所示:由上图可知,在生成的go.mod文件中显示了该module可被导入的路径以及Go的版本。因为目前还没有导入任何其他依赖包,所以没有显示导入包的信息。好,现在我们把该目录同时提交到git上。git init
git remote add origin https://github.com/goxuetang/encodex.git
第三,我们在encodex的hash包中添加如下代码:
好了,到这里我们就可以发布我们的包。但在发布之前我们先来看下语义化的版本。语义化的版本是一种通用的版本格式。其格式如下:vMajor.Minor.Patch
该格式以固定的字母 v 开头,Major代表主版本,Minor代表次版本,Patch代表不定版本。只有在版本不兼容之前的版本时,才会改动主版本Major。当做了向下兼容的功能时会改动Minor。当对次版本Minor做了问题修正时会改动Patch。详细的语义化版本可参考语义化版本官方文档进一步阅读。Go语言指出,当一个module的新老版本不兼容时,新版本应该发布一个新的主版本。同时,Go会认为这是一个独立的module,和之前的老版本没有任何关系。Git的分支本质上是一个历史提交的记录。对于每一次提交都有一个唯一的标识对应。对于每一个唯一标识,我们还可以给一个语义化的版本别名,也就是我们所说的tag。最后,我们可以给我们的module打一个tag了。因为是第一个版本,所以我们使用版本v1.0.0,如下:
git tag v1.0.0
git push --tags
到此,我们的module已经发布了,并由一个v1.0.0的tag版本。接下来,我们看看在项目中如何使用该module2.4 如何使用第三方module
我们在新建的main module中创建了一个main.go文件,在该module下要想使用encodex模块下的包,则需要引入和安装两个步骤。在文件中使用import语句引入包,如下图: 第一步,使用import引入模块下具体的包。因为在encodex的module中,我们设置的引入路径是github.com/goxuetang/encodex, 即go.mod文件的第一行。hash包是encodex模块下的一个包。所以我们引入的完整路径是:import "github.com/goxuetang/encodex/hash"
第二步,使用go get命令安装引入的包。使用go get命令时,可以指定包的具体版本,如下:go get github.com/goxuetang/encodex/hash@v1.0.0
也可以不指定版本,这时go get命令会自动的查找最近的版本,如下:go get github.com/goxuetang/encodex/hash
go get:added github.com/goxuetang/encodex v1.0.0
如图所示:
同时,go get会将引入的包加在go.mod文件中。require中不仅有包名,还有对应的版本号。如下图所示: 好,我们现在来看另外一个问题,下载下来的包存在哪里了。2.5 module存储在哪里当go get将包下载下来后,会将其存储到GOPATH/pkg/mod目录下。通过go env可以查看GOPATH环境变量的具体指向目录,我的环境下的GOPATH=/Users/YuYang/go,如下是上节中引入的encodex模块。如下图所示:我们发现encodex模块的目录是带版本号的,这也是Go module能够支持多版本的原因。三、如何升级版本在上面我们有讲到module使用的是vX.X.X格式的语义化版本。那么在日常的研发中又是如何对这三个版本号进行升级的呢。3.1 如何升级module的小版本和补丁版本随着时间的推移,发布的包肯定会有新的提交,比如修复了一个bug,则patch版本号会升级,添加了一个新功能,则小版本号会升级。做了一项大的改动,和前一个版本不兼容了,那么主版本号就会升级。接下来我们看看在已引入的包后,如何升级对应的版本。如果我们只想升级补丁版本patch,那么可以使用如下命令:go get -u=patch
如果想更新同一个大版本下的小版本,那么可以使用如下命令:
go get -u
该命令是如果小版本有更新,则升级小版本。如果只有补丁版本有更新,则会升级补丁版本。如果想升级到指定的版本,则使用指定版本的命令:go get module@version
例如,要将encodex模块升级到v1.1.3版本,则使用如下命令:
go get github.com/goxuetang/encodex@v1.1.3
3.2 如何升级module的大版本
如果想要升级大版本则需要重新安装大版本,因为在上面我们有提到,在Go中,会将一个大版本视为一个全新的模块。因此,需要使用go get安装该大版本的模块,同时在对应的文件中通过import引入该包。例如encodex模块升级到了v2版本,那么就需要在encodex模块的go.mod中将导入路径更改为v2。如下:github.com/goxuetang/encodex/v2
然后就可以在工程中引用该v2版本的模块了。如下:
import newHash github.com/goxuetang/encodex/v2/hash
同时使用go get命令下载并安装该模块:
go get github.com/goxuetang/encodex/v2
四、间接依赖
一个工程所依赖的模块可分为直接依赖和**间接依赖。**直接依赖就是我们的工程文件中使用import语句导入的模块。而间接依赖就是我们直接依赖的模块所依赖的。如下图: 现在我们在main模块中引入github.com/go-redis/redis 模块,然后查看go.mod文件,发现有如下间接的依赖模块,这里的模块正是在github.com/go-redis/redis 中引入的模块,可以查看github.com/go-redis/redis 模块的go.mod文件以确认。在上图中,我们还发现redis的模块后面的版本是 v6.15.9+incompatible。这个代表什么意思呢?这个代表的是引入的模块的最新版本是v5.15.9,但同时具有不兼容的风险。为什么呢?因为在redis模块中未使用规范的导入名称。例如,规范的模块命名应该是在模块的版本大于1的时候,导入名称就需要增加主版本信息。例如,当该模块是第一个版本时,其对应的go.mod文件如下:module github.com/go-redis/redis
当主版本升级到2时,则go.mod中的模块导入名称应该为:
module github.com/go-redis/redis/v2
如果不增加v2这个标识,那么当使用go get github.com/go-redis/redis 下载包的时候,go会找到模块名称没有使用主版本标识的最新的版本。我们通过查看该模块在git上的6.15.9的版本源码,发现其源码中并没有go.mod文件。所以,当模块的go.mod文件中的导入路径没有版本后缀(例如v2)的情况下,默认是v1版本,因此在使用go get获取这样的模块时,默认会获取v1.x.x的最新版本。五、 小版本的选择
我们已经知道了Go可以同时导入主版本不同的module。那么,如果只有小版本或补丁版本不同,那么Go该如何选择呢?假设工程项目直接依赖于两个module:A和B。同时A依赖于MODULE 1 的v1.0.1版本,但B依赖于MODULE 1的v1.0.2版本。如下图所示: 那么,在工程项目模块(PROJECT MODULE)中需要间接依赖MODULE 1的哪个版本呢?如果我们使用v1.0.1,那么MODULE B有可能会产生异常。在语义化版本中,我们知道小版本或补丁版本应该向后兼容,即v1.0.2是兼容v1.0.1的,所以在PROJECZT MODULE中应该选择MODULE 1的v1.0.2版本。总结
Go module不仅解决了项目代码不再依赖于GOPATH路径,而且还解决了相同module的多版本引入问题。通过本篇文章,相信您对module的创建、发布、版本管理、依赖关系都会有了一个清晰的认识。想要了解更多 Golang 相关的内容,欢迎扫描下方👇 关注 公众号,回复关键词 [实战群] ,就有机会进群和我们进行交流~
评论