Go modules基础精进,六大核心概念全解析
点击一键订阅《云荐大咖》专栏,获取官方推荐精品内容,学技术不迷路!
Go 语言做开发时,路径是如何定义的?Go Mudules又为此带来了哪些改变?本文将会全面介绍Go modules六大核心概念,包括了设计理念与兼容性原则等,掌握这些技术点对于管理和维护Go 模块有重要价值。
上一篇文章中,笔者介绍了如何以经典的 hello world 为例创建一个 Go module 模块,需要说明的是一个模块中是可以包含多个包(package)的,它们是可以被一起发布、打包、版本化的。同时,Go Modules 也可以通过版本管理系统(github、gitlab)或者 goproxy 代理进行下载。在使用 Go Modules 之前,建议大家弄清楚息息相关的六大核心概念,以方便大家在后期的开发、使用过程中理解更加深入。
我们在使用 Go 语言做开发时经常会遇到像 “example.com/test” 或者 “example.com/test/pkg/log”这样的路径,这些路径到底是怎么定义的,两者中存在什么关系,在 Go Modules 中又扮演着怎样的角色呢?Go Modules 的引入对已有的包又引入了哪些新的概念,它们是如何协作的?对兼容性提出了哪些新的要求呢?让我们一起来看一下。
一:模块路径 (Module Path)
Go 使用 “module path” 来区分不同的 module 模块,它在 go.mod 文件中被定义,这个文件中还包含了这个模块编译所需的其他依赖。如果一个目录中包含了 go.mod 文件,那么这个目录就是这个 Go 模块的根目录了。
另外,还要介绍下包(package) 这个概念,它在 Go Modules 出现之前就已经存在了。Go 模块中的 “包 (package)”是处于同一目录中的一些源代码文件的集合,这些文件将被编译在一起。“包路径(package path)”是模块路径和子目录(模块根目录的相对路径)的组合。举个例子,在模块“golang.org/x/net”下的 html 目录中有个包,这个包的路径是 “golang.org/x/net/html”。
总结下来就是: 一个代码仓库可以包含多个 Go 模块,一个 Go 模块可以包含多个 Go 包。
模块路径是一个 Go 模块的规范名称,用于区分不通的模块。同时他还是该模块下 Go 包的路径前缀。理论上,模块路径应该至少包含两个关键信息:
模块的作用 哪里获取该模块
二:版本号与兼容性原则
版本号相当于是一个模块的只读快照,它可以是正式的发布版本,也可以是预发布版本。 每个版本都以字母 v 开头,后跟一个语义版本,例如 v1.0.0。
总而言之,语义版本由三个由点分隔的非负整数(主要版本、次要版本和补丁版本,从左到右)组成。 补丁版本后可以跟一个以连字符开头的可选预发布字符串。 预发布字符串或补丁版本后可以跟一个以加号开头的构建元数据字符串。 例如,v0.0.0、v1.12.134、v8.0.5-pre、v2.0.9+meta 等都是有效版本。
版本号中的信息代表了这个版本是否是一个稳定版,是否保持了与之前版本的兼容性。
- 当维护的模块发生了一些不兼容变更,比如修改了外部可调用的接口或者函数时,需要对主版本号进行递增,并且将次版本号和补丁版本号置为零。比如在模块中移除了一个包。
- 在模块中添加一些新的函数或者接口,并没有影响模块的兼容性时,需要对次版本号进行递增,并且将补丁版本号置为零。
- 当修复了一些 bug 或者进行了一些优化时,只需要对补丁版本号进行递增就可以了,因为这些变更不会对已经公开的接口进行变更。
- 预发布后缀代表了这个版本号是一个预发布版本。预发布版本号的排序会在正式版本号的前面。举个例子,v1.2.3-pre 会排列在 v1.2.3 前面。
- 元数据后缀会在版本比对中被忽略,版本控制中的代码库会忽略带有构建元数据的标签,但在 go.mod 文件中指定的版本中会保留构建元数据。如果一个模块还没有迁移到 Go Modules 并且主版本号是 2 或者更高,+incompatible 后缀会被添加到版本号上。
如果一个版本的主版本号是 0 或者它有一个预发布版本后缀,那么这个版本被认为是一个不稳定版本。通常,不稳定版本不受兼容性限制的,举个例子,v0.2.0 可能和 v0.1.0 是不兼容的,v1.5.0-beta 可能和 v1.5.0 也是不兼容的。
Go 可以通过 tags、分支、和 commit 哈希值来获取模块,即使这些命名没有遵循这些规则。在主模块中,go 命令会自动的将这些 revision 转化为符合标准的版本号,其被称为伪版本号(pseudo-version)。举个例子,当执行下面的命令时:
go get -d golang.org/x/net@daa7c041
Go 会讲指定的 hash daa7c041 转化为一个伪版本号 v0.0.0-20191109021931-daa7c04131f5。在主模块之外需要规范版本,如果 go.mod 文件中出现像 master 这样的非规范版本,go 命令会报错。
三:伪版本号
伪版本号是一种预发布版本号的格式,其中包含了指定的 commit hash 值。另外,对于没有打标签的代码库,也可以使用伪版本号来表明某个版本,它可以在正式发布某个版本之前方便的进行测试。举个例子,每个伪版本号都有三部分组成:
- 基本版本前缀(vX.0.0 或 vX.Y.Z-0),它要么源自修订版之前的语义版本标签,要么源自 vX.0.0(如果没有此类标签)。
- 时间戳 (yyyymmddhhmmss),这是创建 commit 的 UTC 时间。 在 Git 中,这是 commit 提交时间。
- commit 标识符 (abcdefabcdef),它是提交 commit 哈希的 12 个字符的前缀,或者在 Subversion 中,是一个用零填充的修订号。
在这三个部分之下,又分为以下多种情况
- 如果之前没有基版本,那么诸如 vX.0.0-yyyymmddhhmmss-abcdefabcdef 这样的伪版本号将被启用。主版本号 X 需要匹配模块的主版本号后缀。
- 如果之前的基版本号是一个像 vX.Y.Z-pre 这样的预发布版本,那么 vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef 将被采用。
- 如果之前的基版本号是一个像 vX.Y.Z 这样的正式版本,那么 vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef 将被采用,举个例子,如果基版本号是 v1.2.3,伪版本号可能是 v1.2.4-0.20191109021931-daa7c04131f5。
- 基于不同的基础版本号,多个伪版本号是有可能指向同一个 commit hash 的,在对一个低于已经存在的伪版本号打标签时,这种情况就会发生。
上面介绍的这种伪版本号携带了两个非常有用的信息:
1. 伪版本号会高于这些已经存在的基础版本号,但是会低于后面生成的其他伪版本号。
2. 有相同基础版本前缀的伪版本按时间顺序排序。
伪版本号不需要手动指定。很多 Go 命令可以接受一个 commit hash 或者分支名,然后自动将其转化为一个伪版本号(或者一个标签,如果存在的话)。例如:
go get -d example.com/mod@master go list -m -json example.com/mod@abcd1234
四:主版本号后缀
从主版本号 2 开始,模块路径中必须添加一个像 /v2 这样的一个和主版本号匹配的后缀。举个例子如果一个模块在版本 v1.0.0 是的路径为 example.com/test,那么它在 v2.0.0 时的路径将是 example.com/test/v2。
主版本号后缀遵循导入兼容规则:
如果一个新代码包和老代码包拥有同样的导入路径,那么新包必须保证对老代码包的向后兼容。
- 根据定义,模块的新主版本中的包与先前主版本中的相应包不向后兼容。 因此,从 v2 开始,包需要新的导入路径。 这是通过向模块路径添加主版本后缀来实现的。 由于模块路径是模块内每个包的导入路径的前缀,因此将主版本后缀添加到模块路径可为每个不兼容的版本提供不同的导入路径。
- 主版本 v0 或 v1 不允许使用主版本后缀。 v0 和 v1 之间的模块路径不需要更改,因为 v0 版本为不稳定,没有兼容性保证。 此外,对于大多数模块,v1 向后兼容最新的 v0 版本, v1 版本才开始作为对兼容性的承诺。
- 这里有一个特例,以 gopkg.in/ 开头的模块路径必须始终具有主版本后缀,即使是 v0 和 v1 版本。 后缀必须以点而不是斜线开头(例如,gopkg.in/yaml.v2)。因为在 Go Modules 推出之前,gopkg.in 就沿用了这个规则,为了能让引入 gopkg.in 包的代码能继续导入编译, Go 做了一些兼容性工作。
- 主版本后缀可以让一个模块的多个主版本共存于同一个构建中。 这可以很好的解决钻石依赖性问题(diamond dependency conflict) https://jlbp.dev/what-is-a-diamond-dependency-conflict。 通常,如果传递依赖项在两个不同版本中需要一个模块,则将使用更高的版本。 但是,如果两个版本不兼容,则任何一个版本都不会满足所有的调用者。 由于不兼容的版本必须具有不同的主版本号,因此主版本后缀具有不同的模块路径,这样就不存在冲突了:具有不同后缀的模块被视为单独的模块,并且它们的包的导入路径也是不同的。
- 因为很多 Go 项目在迁移到 Go 模块之前就发布了 v2 或更高版本的版本,所以没有使用主要版本后缀。对于这些版本,Go 使用 +incompatible 构建标记来进行注释(例如,v2.0.0+incompatible)。
五:解析包路径到模块路径的流程
通常在使用“go get”时可能是指定到一个包路径,而非模块路径,Go 是如何找到模块路径的呢?
go 命令会在主模块(当前模块)的 build list 中搜索有哪些模块路径匹配这个包路径的前缀。举个例子,如果导入的包路径是 example.com/a/b,发现 example.com/a 是一个模块路径,那么就会去检查 example.com/a 在 b 目录中是否包含这个包,在这个目录中要至少存在一个 go 源码文件才会被认为是一个有效的包。编译约束(Build Constraints)在这一过程中不会被应用。 如果确实在 build list 中找到了一个模块包含这个包,那么这个模块将被使用。如果没有发现模块能提供这个包或者发现两个及两个以上的模块提供了这个包,那么 go 命令会提示报错。但是你可以指定 -mod=mod 来使 go 命令尝试下载本地找不到的包,并且更新 go.mod 和 go.sum。go get 和 go mod tidy 这两个命令会自动的做这些工作。
当 go 命令试图下载一个新的代码包时,它回去检查 GOPROXY 环境变量,这是一个使用逗号分隔的 URL 列表,当然也支持像 direct 和 off 这样的关键字。代理 URL 代表 go 将使用 GOPROXY 协议拉取模块,direct 表示 go 需要和版本控制系统直接交互,off 不需要和外界做任何交互。另外,GOPRIVATE 和 GONOPROXY 环境变量也可以精细的控制 go 下载代码包的策略。
对于 GOPROXY 列表中的每一项, go 命令回去请求模块路径的每一个前缀。对于请求成功的模块,go 命令回去下载最新模块并且检查这个某块是否包含请求的包。如果多个模块包含了请求的包,拥有最长路径的将被选择。如果发现的模块中没有包含这个包,会报错。如果没有模块被发现,go 命令会尝试 GOPROXY 列表中的下一个配置项,如果最终都尝试过没有发现则会报错。举个例子,假设用户想要去获取 golang.org/x/net/html 这个包,之前配置的 GOPROXY 为 https://corp.example.com,https://goproxy.io。go 命令会遵循下面的请求顺序:
向 https://corp.example.com/ 发起请求 (并行): Request for latest version of golang.org/x/net/html Request for latest version of golang.org/x/net Request for latest version of golang.org/x Request for latest version of golang.org
如果 https://corp.example.com/ 上面都失败了返回 410 或者 404 状态码,向 https://proxy.golang.org/ 发起请求:
Request for latest version of golang.org/x/net/html Request for latest version of golang.org/x/net Request for latest version of golang.org/x Request for latest version of golang.org
当一个需要的模块被发现后,go 命令会将这个依赖模块的路径和对应版本添加到主模块的 go.mod 文件中。这样就确保了以后在编译该模块时,同样的模块版本将被使用,保证了编译的可重复性。如果解析的代码包没有被主模块直接引用,在 go.mod 文件中添加的新依赖后会有 // indirect 注释。
六:go.mod 文件
就像前面提到过的,模块的定义是由一个 UTF-8 编码的名为 go.mod 文本文件定义的。 这个文件是按照“行”进行组织的(line-oriented)。每一行都有一个独立的指令,有一个预留关键字和一些参数组成。比如:
module example.com/my/thing go 1.17 require example.com/other/thing v1.0.2 require example.com/new/thing/v2 v2.3.4 exclude example.com/old/thing v1.2.3 replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5 retract [v1.9.0, v1.9.5]
开头的关键词可以以行的形式被归总为块,就像日常所用的 imports 一样,所以可以改成下面这样:
require ( example.com/new/thing/v2 v2.3.4 example.com/old/thing v1.2.3 )
go.mod 文件的设计兼顾了开发者的可读性和机器的易写性。go 命令也提供了几个子命令来帮组开发者修改 go.mod 文件。举个例子,go get 命令可以在需要的时候更新 go.mod 文件。go mod edit 命令可以对文件做一些底层的修改操作。如果我们也有类似的需求,可以使用 golang.org/x/mod/modfile 包以编程方式进行同样的更改。通过这个包,也可以一窥底层 go.mod 的 struct 结构:
// go.mod 文件的组成形式 type File struct { Module *Module // 模块路径 Go *Go // Go 版本 Require []*Require // 依赖模块 Exclude []*Exclude // 排除模块 Replace []*Replace // 替换模块 Retract []*Retract // 撤回模块 } // A Module is the module statement. type Module struct { Mod module.Version Deprecated string } // A Go is the go statement. type Go struct { Version string // "1.23" } // An Exclude is a single exclude statement. type Exclude struct { Mod module.Version } // A Replace is a single replace statement. type Replace struct { Old module.Version New module.Version } // A Retract is a single retract statement. type Retract struct { VersionInterval Rationale string }
从上面的 Module 的 struct 中可以看到 “Deprecated”这一结构,在 Go Modules 推出的早期是没有这个设计的,那么这个字段是做什么用的呢? 估计很多人都不知道,如果我们维护的一个模块主版本从 v1 演进到了 v2,而不再维护 v1 版本了,希望用户尽可能使用 v2,通过上面的介绍知道v1 和 v2 是不同的 import path,“Retract”也无能为力,这时候这个 “Deprecated”就起作用了,看下面的例子:
// Deprecated: in example.com/a/b@v1.9.0, the latest supported version is example.com/a/b/v2. module example.com/a/b go 1.17
当用户再去获取 example.com/a/b 这个版本时,go 命令可以感知到这个版本已经不再维护了,会报告给用户:
go get -d example.com/a/b@v1.9.0 go: warning: module example.com/deprecated/a is deprecated: in example.com/a/b@v1.9.0, the latest supported version is example.com/a/b/v2
用户就可以根据提示进行 v2 代码拉取了。
《Go modules基础精进,六大核心概念全解析》一文全面介绍了 Go Modules 中的模块、模块路径、包、包路径、如何通过包路径寻找模块路径,还介绍了版本号和伪版本号,最后简单介绍了 go.mod 文件,以及其中不为人知的“Deprecated”功能,了解这些概念、设计理念和兼容性原则,将对管理和维护自己的 Go 模块大有帮助。
以上这些概念都是平常使用 Go 语言会高频接触到的内容,理解版本号和伪版本号的区别和设计原则,可以帮助我们清楚按照 semver 的标准定义自己的 tag 是多么重要。同时,遵循Go Modules 定义的兼容性原则,上下游开发者在社区协同时将会变得更加友好和高效。接下来的系列文章将会开始具体来了解 Go Modules 中的设计细节,例如 go.mod 文件详解以及配套的 go mod 子命令等,敬请期待。另外,腾讯云 goproxy 企业版已经产品化,需要了解的同学可以点击这里。
李保坤往期精彩文章推荐:Go语言重新开始,Go Modules 的前世今生与基本使用
《云荐大咖》是腾讯云加社区精品内容专栏。云荐官特邀行业佼者,聚焦于前沿技术的落地及理论实践之上,持续为您解读云时代热点技术、探索行业发展新机。点击一键订阅,我们将为你定期推送精品内容。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
华工安鼎加入欧拉开源社区
近日,武汉华工安鼎信息技术有限责任公司(以下简称“华工安鼎”)签署 CLA(Contributor License Agreement 贡献者许可协议),正式加入欧拉开源社区,未来将积极参与社区合作,与行业内的生态伙伴共建创新平台,推动行业生态繁荣发展。 华工安鼎成立于 1999 年,是专业提供信息安全产品及信息安全服务的高新技术企业,华工安鼎在数据安全领域不懈努力,持续成长,已在数据安全、云安全和人工智能方面形成了具有特色的系列安全产品和解决方案。可为政府、军工、央企等重点行业及领域提供以分级保护、等级保护、商业秘密保护为核心的信息安全整体解决方案与系列产品。 华工安鼎在信息安全领域潜心耕耘,20 年磨一剑,构建了以数据标识为核心技术的文档全生命周期保护、文档智能分类分级、数据脱敏及隐写溯源、文档大数据态势感知、云原生安全等解决方案,为行业用户提供以数据为核心的自主创新、自主可控的一流产品和服务,加入欧拉开源社区,华工安鼎期待与业界同仁一起,为国家信息安全做出更大的贡献。
- 下一篇
EdgeX Foundry v2.1 作为第一个长期支持版(LTS)发布
2021年12月1日,Linux 基金会下的边缘基金会(LF Edge)宣布发布代号为“Jakarta”(雅加达)的 EdgeX v2.1 版。这是该项目继最近的爱尔兰版本之后发布的第九个版本,后者是该项目的第二个主要版本(v2.0版)。 Jakarta 的重要性在于:它是EdgeX 的第一个提供长期支持(LTS)版本。 长期支持 我们的Jakarta版本是一个稳定版本,”EdgeX Foundry技术指导委员会(TSC)主席兼该项目的联合创始人Jim White说:“如您所见,EdgeX提供稳定版本的平台是我们项目社区对采用者的承诺,您可以期望社区支持并提供两年的支持。我们与您站在一起,在现实世界中支持EdgeX平台的商业部署。” “只有少数开源项目提供长期支持版。”Linux基金会网络、边缘和物联网总经理Arpit Joshipura表示:“开源项目的快速变化以及提供长期支持版所需的努力非常巨大。通过提供长期支持,EdgeX证明它了解运营技术(OT)用户群的需求,以及该领域的产品必须如何比传统IT解决方案工作和运行更长时间。对于任何开源社区来说,这都是一个重要的里程碑,我们为Edg...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS关闭SELinux安全模块
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境