Go Module 的设计采用了语义化版本规范,语义化版本规范非常流行且具有指导意义,本文就来聊聊语义化版本规范的设计和在 Go 中的应用。
语义化版本规范
语义化版本规范(SemVer)是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立,旨在解决 依赖地狱
问题。
它清楚明了的规定了版本格式、版本号递增规:
版本格式:采用 X.Y.Z 的格式,X 是主版本号、Y 是次版本号、而 Z 为修订号(即:主版本号.次版本号.修订号),其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。
版本号递增规则:
主版本号:当做了不兼容的 API 修改。
次版本号:当做了向下兼容的功能性新增及修改。
修订号:当做了向下兼容的问题修正。
另外,先行版本号
及 版本编译信息
可以加到 主版本号.次版本号.修订号
的后面,作为延伸。
完整版本格式如下:
其中版本号核心部分 X.Y.Z 是必须的,使用 .
连接,先行版本号和版本编译信息是可选的,先行版本号通过 -
与核心部分连接,版本编译信息通过 +
与核心部分或先行版本号连接。
合法的几种版本号格式如下:
- 主版本号.次版本号.修订号
- 主版本号.次版本号.修订号-先行版本号
- 主版本号.次版本号.修订号+版本编译信息
- 主版本号.次版本号.修订号-先行版本号+版本编译信息
主版本号必须在有任何不兼容的修改被加入公共 API 时递增。每当主版本号递增时,次版本号和修订号必须归零。
次版本号必须在有向下兼容的新功能出现或有改进时递增,或在任何公共 API 的功能被标记为弃用时也必须递增。每当次版本号递增时,修订号必须归零。
修订号必须在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。
存在先行版本号,意味着当前版本不够稳定,且可能存在兼容性问题。先行版本号是一连串以 .
分隔的标识符,由 ASCII 字母数字和连接号 [0-9A-Za-z-]
组成,禁止出现空白符,数字类型则禁止在前方补零。合法示例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。
版本编译信息标志符规格与先行版本号基本相同,略有差异的是数字类型前方允许补零。合法示例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。
除了上面几点说明,还需要额外关注以下几点:
- 标记版本号的软件发行后,禁止改变该版本软件的内容。任何修改都必须以新版本发行。
- 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。
- 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。
- 社区中还存在一个不成文的规定,对于次版本号,偶数为稳定版本,奇数为开发版本。当然不是所有项目都这样设计。
使用语义化版本规范可能遇到的问题
在使用语义化版本规范过程中,可能人为或程序编写错误导致出现如下几种可预见的问题:
- 万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?
一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。
注意:不到万不得已,不要也不能去修改已发行的版本。
- 如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)
自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住,语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。
v1.2.3
是一个语义化版本号吗?
v1.2.3
并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 v
是用来表示版本号的常用做法。在版本控制系统中,将 version
缩写为 v
是很常见的。比如:git tag v1.2.3 -m "Release version 1.2.3"
中,v1.2.3
表示标签名称,而 1.2.3
是语义化版本号。
如何验证语义化版本规范正确性
官方提供了两个正则可以检查语义化版本号的正确性。
- 支持按组名称提取匹配结果
1 |
^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ |
Go 语言示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package main import ( "encoding/json" "fmt" "regexp" ) funcmain() { version := "0.1.2-alpha+001" pattern := regexp.MustCompile(`^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) r := pattern.FindStringSubmatch(version) m := make(map[string]string) for i, name := range pattern.SubexpNames() { if i == 0 { m["version"] = r[i] } else { m[name] = r[i] } } result, _ := json.MarshalIndent(m, "", " ") fmt.Printf("%s\n", result) } /* { "buildmetadata": "001", "major": "0", "minor": "1", "patch": "2", "prerelease": "alpha", "version": "0.1.2-alpha+001" } */ |
- 支持按编号提取匹配结果
1 |
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ |
Go 语言示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "regexp" ) funcmain() { version := "0.1.2-alpha+001" pattern := regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) r := pattern.FindStringSubmatch(version) for i, s := range r { fmt.Printf("%d -> %s\n", i, s) } } /* 0 -> 0.1.2-alpha+001 1 -> 0 2 -> 1 3 -> 2 4 -> alpha 5 -> 001 */ |
Go Module 版本设计
依赖地狱
我们先来看下早期 Go 依赖包存在的依赖地狱问题:
依赖地狱
首先存在两个包 pkg1
和 pkg2
,分别依赖 pkg3
的 v1.0.0
版本和 v2.0.0
版本,现在我们开发一个 app
包,它依赖 pkg1
和 pkg2
,那么此时由于 app
包只允许包含一个 pkg3
依赖,所以 Go 构建工具无法抉择应该使用哪个版本的 pkg3
。这就是所谓的依赖地狱问题。
语义导入版本
为了解决依赖地狱问题,Go 在 1.11 版本时引入和 Go Module:
Go Module
Go Module 解决问题的方式是,把 pkg3
的 v1.0.0
版本和 v2.0.0
版本当作两个不同的包,这样也就允许了 app
包能够同时包含多个不同版本的 pkg3
。
在使用时,需要在包的导入路径上加上包的主版本号。这里以 go-micro
包使用为例,展示下 Go Module 语义导入版本的用法:
1 2 3 4 5 6 7 8 9 10 11 12 |
import"go-micro.dev/v4" // create a new service service := micro.NewService( micro.Name("helloworld"), ) // initialise flags service.Init() // start the service service.Run() |
可以看到导入路径为 "go-micro.dev/v4"
,其中 v4
就代表了需要引入 go-micro
的 v4.y.z
版本。
参考