Go Module 语义化版本规范

简介: Go Module 语义化版本规范

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. 支持按组名称提取匹配结果
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. 支持按编号提取匹配结果
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 依赖包存在的依赖地狱问题:

依赖地狱

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

语义导入版本

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

Go Module

Go Module 解决问题的方式是,把 pkg3v1.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-microv4.y.z 版本。

参考

https://github.com/semver/semver

https://semver.org/lang/zh-CN/

相关文章
|
1月前
|
测试技术 Go 开发者
go-carbon v2.3.8 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
36 0
|
8月前
|
存储 Go
Go语言接口声明规范和最佳实践
Go语言接口声明规范和最佳实践
65 0
|
9月前
|
Go 开发者
go-carbon 2.2.7 版本发布, 轻量级、语义化、对开发者友好的Golang时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
41 0
|
22天前
|
Go 开发者
GVM:Go语言版本和包管理的神器!
GVM,Go版本管理器,简化了在单机上切换不同Go版本的任务。
14 0
|
1月前
|
Go
Golang深入浅出之-Go语言代码质量与规范:遵循Gofmt与Linting
【5月更文挑战第1天】本文讨论了如何使用`gofmt`和Lint工具提升Go代码质量。`gofmt`负责自动格式化代码,保持风格统一,而Lint工具如`golint`、`govet`、`staticcheck`则进行静态分析,检查潜在错误和未使用的变量。通过集成`gofmt`检查到CI/CD流程,避免格式冲突,并使用Lint工具发现并修复问题,如未处理的错误、不规范命名。遵循这些最佳实践,可提高代码可读性、团队协作效率和可维护性。
42 3
|
6月前
|
Dubbo 应用服务中间件 API
Go语言微服务框架重磅升级:dubbo-go v3.2.0 -alpha 版本预览
随着 Dubbo3 在云原生微服务方向的快速发展,Dubbo 的 go 语言实现迎来了 Dubbo3 版本以来最全面、最大幅度的一次升级,这次升级是全方位的,涉及 API、协议、流量管控、可观测能力等。
|
8月前
|
存储 Go API
怎么发布 Go Modules v1 版本?
怎么发布 Go Modules v1 版本?
41 0
|
1月前
|
JSON Go 开发者
go-carbon v2.3.0 圣诞特别版发布,轻量级、语义化、对开发者友好的 Golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
56 0
|
7月前
|
测试技术 Go 开发者
go-carbon 2.2.12 版本发布, 轻量级、语义化、对开发者友好的 Golang 时间处理库
carbon 是一个轻量级、语义化、零依赖、对开发者友好的 golang 时间处理库,支持链式调用。
33 1
|
6月前
|
测试技术 Go 开发者
go-carbon v2.2.14 发布,轻量级、语义化、对开发者友好的 Golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
34 0