手把手教你如何创建及使用Go module

yudotyang · · 3657 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

大家好,我是「Go学堂」的渔夫子。今天跟大家聊聊go中的module。 原文链接:https://mp.weixin.qq.com/s/JpE5aIl2Lu0T1mEwksKw_Q Go module是从Go 1.11版本才引入的新功能。其目标是取代旧的的基于GOPATH方法来指定在工程中使用哪些源文件或导入包。本文首先分析Go引入module之前管理依赖的优缺点,然后针对这些缺点,看module是如何解决的。 **一、传统的包管理方式-package** 在Go1.11之前,如果想要编写Go代码以及引入第三方包,则需要将**源代码写在GOPATH/src目录下**。即开发者只能将研发的项目放到GOPATH目录下。同时,将引入的第三方包会下载到GOPATH/pkg目录下**。**我们先来看下在这种包管理模式下,使用go get是如何安装依赖包的,然后再分析这种包管理的不足。 **1.1 go get的工作流程** 我们以在项目中引入github.com/go-redis/redis包为例。在项目中使用import导入该包: ```go import "github.com/go-redis/redis" ``` 然后我们需要使用go get命令将该包下载下来: ```go 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目前支持的在线版本管理类型如下: ```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 <meta name="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。 根据读取到的meta信息,Go就可以从 https://github.com/go-redis/redis.git 中克隆该项目代码,并将其保存到本地的$GOPATH/src目录下的github.com/go-redis/redis中。 ![01-git-import-redis.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90e841c7b88e4ec6a02ce674122ca1d3~tplv-k3u1fbpfcp-zoom-1.image) 到此,我们已经了解了传统的包管理的工作方式了。下面我们来看看这种管理方式有哪些缺点。 **1.2 传统包管理方式的不足** **首先,所有的项目都必须在GOPATH/src指向的目录下,或者必须更改GOPATH环境变量所指向的目录**。 我们以两个项目A、B来举例说明。假设当前的GOPATH=/usr/local/goworkspace/。如果保持GOPATH不变的话,那么A、B两个项目的源代码都必须要放到GOPATH的目录下,即/usr/local/goworkspace/src目录下。同时,A和B项目引入的第三方包都会在GOPATH/pkg目录下。这样两个项目其实就是混合在一起。 如果不想混合在一起怎么办呢?那就只能更改GOPATH的目录。假设我们现在在研发A项目,并将其工作目录放在/usr/local/goworkspace/a目录下,GOPATH=/usr/local/goworkspace/a。但是在开发B项目时,更改GOPATH的指向,例如我们这里使用/usr/local/goworkspace/b目录下。这样两个项目的源代码以及依赖的第三方包就在各自项目下了。 但同时如果想继续修改A项目的代码时,就需要再将GOPATH目录更改到指向A项目的目录中,即GOPATH=/usr/local/goworkspace/src目录。 **其次,对于依赖的同一个包只能从master分支上导入最新的提交,且不能导入包的指定的版本**。 假设我们有一个第三方包redis,项目A首次引入该包时,使用go get命令从代码库的master分支下载当前最新的代码,并将该包保存在本地的GOPATH/pkg目录下。之后redis包有了新的提交,但同时也引入了一个bug。如果项目A升级或重新安装该包时,使用go get命令并没有指定特定版本的参数,还是从该包的代码库的master分支中下载该包,也就造成了向后不兼容。另外,升级或重新安装的包也会被安装到GOPATH/pkg下的相同目录,因为没有版本的管理,所以会覆盖之前。 好了,以上就是在传统的包管理方式中的两大主要不足之处。那么针对这些不足,我们来看看Go的module是如何解决的。 **二、现代包管理方式-module** **2.1 什么是module** **一个module就是一个包含多个package的目录,即一个package的集合。** 其要实现的目标如下: - 首先,研发者应该能够在任何目录下工作,而不仅仅是在GOPATH指定的目录。 - 可以安装依赖包的指定版本,而不是只能从master分支安装最新的版本。 - 可以导入同一个依赖包的多个版本。当我们老项目使用老版本,新项目使用新版本时会非常有用。 - 要有一个能够罗列当前项目所依赖包的列表。这个的好处是当我们发布项目时不用同时发布所依赖的包。Go能够根据该文件自动下载对应的包。 一个module也是可以像package一样共享的。因此,module也必须是一个git仓库或其他Go可支持的代码控制系统。因此,Go的建议是: - 一个module必须是一个代码控制系统的仓库,并且一个仓库应该只能包含一个module。 - 一个module应该包含一个或多个package。 - 一个包应该在同一个目录下包含一个或多个go文件 **2.2 如何创建module** **第一,我们在GOPATH之外的任何位置创建一个目录**。 这里我们使用encodex,该encodex包含一些对字符串的编码功能函数,例如md5,sha1等。如下图: ![02-创建module示例-01.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d5eeab350b245d8a3d2127743b53b42~tplv-k3u1fbpfcp-zoom-1.image) 根据上面所讨论的,一个Go module应该是一个版本控制系统上的代码仓库。所以我们在github上创建一个git的代码仓库,如下图: ![03-创建module示例-02-git的创建.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd3502dfe02e40b3a53828c6af1b0bf3~tplv-k3u1fbpfcp-zoom-1.image) **第二,在本地的目录下执行go mod init <import-path>命令来初始化Go module**。 ```go go mod init github.com/goxuetang/encodex ``` 该命令会在encodex的根目录下创建go.mod文件,go.mod文件会包含我们定义的module的导入路径和依赖的包及对应的版本。如下所示: ![04-显示go-mod-init.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b424fdc398814c949446f19b197c4eec~tplv-k3u1fbpfcp-zoom-1.image) 由上图可知,在生成的go.mod文件中显示了该module可被导入的路径以及Go的版本。因为目前还没有导入任何其他依赖包,所以没有显示导入包的信息。好,现在我们把该目录同时提交到git上。 ```go git init git remote add origin https://github.com/goxuetang/encodex.git ``` **第三,我们在encodex的hash包中添加如下代码**: ![05-hash包截图.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aae16ebab85f49b5ba7f14220c30161c~tplv-k3u1fbpfcp-zoom-1.image) 好了,到这里我们就可以发布我们的包。但在发布之前我们先来看下语义化的版本。 语义化的版本是一种通用的版本格式。其格式如下: ```go vMajor.Minor.Patch ``` 该格式以固定的字母 v 开头,Major代表主版本,Minor代表次版本,Patch代表不定版本。只有在版本不兼容之前的版本时,才会改动主版本Major。当做了向下兼容的功能时会改动Minor。当对次版本Minor做了问题修正时会改动Patch。详细的语义化版本可参考[语义化版本官方文档](https://semver.org/lang/zh-CN/)进一步阅读。 Go语言指出,当一个module的新老版本不兼容时,新版本应该发布一个新的主版本。同时,Go会认为这是一个独立的module,和之前的老版本没有任何关系。 **Git的分支本质上是一个历史提交的记录。对于每一次提交都有一个唯一的标识对应。对于每一个唯一标识,我们还可以给一个语义化的版本别名,也就是我们所说的**[**tag**](https://git-scm.com/book/en/v2/Git-Basics-Tagging)。 **最后,我们可以给我们的module打一个tag了**。 因为是第一个版本,所以我们使用版本v1.0.0,如下: ```go git tag v1.0.0 git push --tags ``` 到此,我们的module已经发布了,并由一个v1.0.0的tag版本。接下来,我们看看在项目中如何使用该module **2.4 如何使用第三方module** 我们在新建的main module中创建了一个main.go文件,在该module下要想使用encodex模块下的包,则需要**引入和安装**两个步骤。在文件中使用import语句引入包,如下图: ![06-main中引入encodex.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a8416a22193493fb8a5abf2b5e232ec~tplv-k3u1fbpfcp-zoom-1.image) **第一步,使用import引入模块下具体的包**。因为在encodex的module中,我们设置的引入路径是github.com/goxuetang/encodex, 即go.mod文件的第一行。hash包是encodex模块下的一个包。所以我们引入的完整路径是: ```go import "github.com/goxuetang/encodex/hash" ``` **第二步,使用go get命令安装引入的包**。使用go get命令时,可以指定包的具体版本,如下: ```go go get github.com/goxuetang/encodex/hash@v1.0.0 ``` 也可以不指定版本,这时go get命令会自动的查找**最近的版本**,如下: ```go go get github.com/goxuetang/encodex/hash go get:added github.com/goxuetang/encodex v1.0.0 ``` 如图所示: ![07-main中使用go-get自动获取版本.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9d12105253c4e2eac021443acf0eced~tplv-k3u1fbpfcp-zoom-1.image) 同时,go get会将引入的包加在go.mod文件中。require中不仅有包名,还有对应的版本号。如下图所示: ![08-go-get的mod文件.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0e00016637145e7a0708c551c3a0178~tplv-k3u1fbpfcp-zoom-1.image) 好,我们现在来看另外一个问题,下载下来的包存在哪里了。 **2.5 module存储在哪里** 当go get将包下载下来后,会将其存储到GOPATH/pkg/mod目录下。通过go env可以查看GOPATH环境变量的具体指向目录,我的环境下的GOPATH=/Users/YuYang/go,如下是上节中引入的encodex模块。如下图所示: ![09-module存储未知.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd5e9a09f299417a8e8efaf865a3ed97~tplv-k3u1fbpfcp-zoom-1.image) 我们发现encodex模块的目录是带版本号的,这也是Go module能够支持多版本的原因。 **三、如何升级版本** 在上面我们有讲到module使用的是vX.X.X格式的语义化版本。那么在日常的研发中又是如何对这三个版本号进行升级的呢。 **3.1 如何升级module的小版本和补丁版本** 随着时间的推移,发布的包肯定会有新的提交,比如修复了一个bug,则patch版本号会升级,添加了一个新功能,则小版本号会升级。做了一项大的改动,和前一个版本不兼容了,那么主版本号就会升级。接下来我们看看在已引入的包后,如何升级对应的版本。 如果我们只想升级补丁版本patch,那么可以使用如下命令: ```go go get -u=patch ``` 如果想更新同一个大版本下的小版本,那么可以使用如下命令: ```go go get -u ``` 该命令是如果小版本有更新,则升级小版本。如果只有补丁版本有更新,则会升级补丁版本。 如果想升级到指定的版本,则使用指定版本的命令: ```go go get module@version ``` 例如,要将encodex模块升级到v1.1.3版本,则使用如下命令: ```go go get github.com/goxuetang/encodex@v1.1.3 ``` **3.2 如何升级module的大版本** 如果想要升级大版本则需要重新安装大版本,因为在上面我们有提到,在Go中,会将一个大版本视为一个全新的模块。因此,需要使用go get安装该大版本的模块,同时在对应的文件中通过import引入该包。例如encodex模块升级到了v2版本,那么就需要在encodex模块的go.mod中将导入路径更改为v2。如下: ```go github.com/goxuetang/encodex/v2 ``` 然后就可以在工程中引用该v2版本的模块了。如下: ```go import newHash github.com/goxuetang/encodex/v2/hash ``` 同时使用go get命令下载并安装该模块: ```go go get github.com/goxuetang/encodex/v2 ``` **四、间接依赖** 一个工程所依赖的模块可分为**直接依赖**和**间接依赖。**直接依赖就是我们的工程文件中使用import语句导入的模块。而间接依赖就是我们直接依赖的模块所依赖的。如下图: ![12-直接依赖和间接依赖.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a8189a28fb224057933f9f3f5a45daea~tplv-k3u1fbpfcp-zoom-1.image) 现在我们在main模块中引入github.com/go-redis/redis 模块,然后查看go.mod文件,发现有如下间接的依赖模块,这里的模块正是在github.com/go-redis/redis 中引入的模块,可以查看github.com/go-redis/redis 模块的go.mod文件以确认。 ![10-间接依赖.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f32fc6228b84619944da345bf2847f2~tplv-k3u1fbpfcp-zoom-1.image) 在上图中,我们还发现redis的模块后面的版本是 v6.15.9+incompatible。这个代表什么意思呢?这个代表的是引入的模块的最新版本是v5.15.9,但同时具有不兼容的风险。 为什么呢?因为在redis模块中未使用规范的导入名称。例如,规范的模块命名应该是在模块的版本大于1的时候,导入名称就需要增加主版本信息。例如,当该模块是第一个版本时,其对应的go.mod文件如下: ```go module github.com/go-redis/redis ``` 当主版本升级到2时,则go.mod中的模块导入名称应该为: ```go module github.com/go-redis/redis/v2 ``` 如果不增加v2这个标识,那么当使用go get github.com/go-redis/redis 下载包的时候,go会找到模块名称没有使用主版本标识的最新的版本。我们通过查看该模块在git上的6.15.9的版本[源码](https://github.com/go-redis/redis/releases/tag/v6.15.8),发现其源码中并没有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版本。如下图所示: ![13-间接依赖多个版本.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3181a12870e4058b5f243173dea8bae~tplv-k3u1fbpfcp-zoom-1.image) 那么,在工程项目模块(PROJECT MODULE)中需要间接依赖**MODULE 1**的哪个版本呢?如果我们使用v1.0.1,那么**MODULE B**有可能会产生异常。在语义化版本中,我们知道小版本或补丁版本应该向后兼容,即v1.0.2是兼容v1.0.1的,所以在PROJECZT MODULE中应该选择MODULE 1的v1.0.2版本。 ![14-最终间接版本选择.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc289d6b48494581a108f1f32c91c030~tplv-k3u1fbpfcp-zoom-1.image) **总结** Go module不仅解决了项目代码不再依赖于GOPATH路径,而且还解决了相同module的多版本引入问题。通过本篇文章,相信您对module的创建、发布、版本管理、依赖关系都会有了一个清晰的认识。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3657 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传