写点什么

Go Module 语义化版本规范

作者:江湖十年
  • 2023-05-29
    浙江
  • 本文字数:3125 字

    阅读完需:约 10 分钟

Go Module 的设计采用了语义化版本规范,语义化版本规范非常流行且具有指导意义,本文就来聊聊语义化版本规范的设计和在 Go 中的应用。

语义化版本规范

语义化版本规范(SemVer)是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立,旨在解决 依赖地狱 问题。


它清楚明了的规定了版本格式、版本号递增规:


版本格式:采用 X.Y.Z 的格式,X 是主版本号、Y 是次版本号、而 Z 为修订号(即:主版本号.次版本号.修订号),其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。


版本号递增规则:


主版本号:当做了不兼容的 API 修改。


次版本号:当做了向下兼容的功能性新增及修改。


修订号:当做了向下兼容的问题修正。


另外,先行版本号版本编译信息 可以加到 主版本号.次版本号.修订号 的后面,作为延伸。


完整版本格式如下:



其中版本号核心部分 X.Y.Z 是必须的,使用 . 连接,先行版本号和版本编译信息是可选的,先行版本号通过 - 与核心部分连接,版本编译信息通过 + 与核心部分或先行版本号连接。


合法的几种版本号格式如下:


  1. 主版本号.次版本号.修订号

  2. 主版本号.次版本号.修订号-先行版本号

  3. 主版本号.次版本号.修订号+版本编译信息

  4. 主版本号.次版本号.修订号-先行版本号+版本编译信息


主版本号必须在有任何不兼容的修改被加入公共 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。


除了上面几点说明,还需要额外关注以下几点:


  1. 标记版本号的软件发行后,禁止改变该版本软件的内容。任何修改都必须以新版本发行。

  2. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  3. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  4. 社区中还存在一个不成文的规定,对于次版本号,偶数为稳定版本,奇数为开发版本。当然不是所有项目都这样设计。

使用语义化版本规范可能遇到的问题

在使用语义化版本规范过程中,可能人为或程序编写错误导致出现如下几种可预见的问题:


  1. 万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?


一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文档中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。


注意:不到万不得已,不要也不能去修改已发行的版本。


  1. 如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)


自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住,语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。


  1. 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 语言示例:


package main
import ( "encoding/json" "fmt" "regexp")
func main() { 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 语言示例:


package main
import ( "fmt" "regexp")
func main() { 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+0011 -> 02 -> 13 -> 24 -> alpha5 -> 001*/
复制代码

Go Module 版本设计

依赖地狱

我们先来看下早期 Go 依赖包存在的依赖地狱问题:



首先存在两个包 pkg1pkg2,分别依赖 pkg3v1.0.0 版本和 v2.0.0 版本,现在我们开发一个 app 包,它依赖 pkg1pkg2,那么此时由于 app 包只允许包含一个 pkg3 依赖,所以 Go 构建工具无法抉择应该使用哪个版本的 pkg3。这就是所谓的依赖地狱问题。

语义导入版本

为了解决依赖地狱问题,Go 在 1.11 版本时引入和 Go Module:



Go Module 解决问题的方式是,把 pkg3v1.0.0 版本和 v2.0.0 版本当作两个不同的包,这样也就允许了 app 包能够同时包含多个不同版本的 pkg3


在使用时,需要在包的导入路径上加上包的主版本号。这里以 go-micro 包使用为例,展示下 Go Module 语义导入版本的用法:


import "go-micro.dev/v4"
// create a new serviceservice := micro.NewService( micro.Name("helloworld"),)
// initialise flagsservice.Init()
// start the serviceservice.Run()
复制代码


可以看到导入路径为 "go-micro.dev/v4",其中 v4 就代表了需要引入 go-microv4.y.z 版本。

希望此文能对你有所帮助。


联系我


参考


https://github.com/semver/semverhttps://semver.org/lang/zh-CN/

发布于: 刚刚阅读数: 3
用户头像

江湖十年

关注

野生程序员 2018-11-10 加入

分享不限于 Go、Python、Docker、K8s 技术。

评论

发布
暂无评论
Go Module 语义化版本规范_Go 语言_江湖十年_InfoQ写作社区