Go Modules基础精进,六大核心概念全解析(下)
导语 | 腾讯云加社区精品内容栏目《云荐大咖》,特邀行业佼者,聚焦前沿技术的落地与理论实践,持续为您解读云时代热点技术,探秘行业发展新机。
在上篇《Go Modules基础精进,六大核心概念全解析(上)》中,我们介绍了模块路径、版本号与兼容性原则、伪版本号三大概念,而在下篇我们将会继续介绍Go Modules核心概念。
四、主版本号后缀
从主版本号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企业版已经产品化,需要了解的同学可以跳转↓↓
(https://goproxy.io/zh/docs/enterprise.html)
推荐阅读
👇戳「阅读原文」一键订阅《云荐大咖》专栏,看云端技术起落,听大咖指点迷津!云荐官将在每周五抽取部分订阅小伙伴,送出云加视频礼盒!