编程 SemVer 2.0.0 优先级比较:alpha.10 排在 alpha.9 之前,以及五个实现坑

2026-10-16 00:04:24

SemVer 2.0.0 优先级比较:alpha.10 排在 alpha.9 之前,以及五个实现坑

规范原文:,英文版 。下面只挑和「比较 / 解析」直接相关的部分。

规范里的 MUST

  • 版本号 MUST 是 X.Y.Z,X/Y/Z 为非负整数,MUST NOT 带前导零。依次是主版本、次版本、修订号。
  • 修订号 Z 只在做向后兼容的 bug 修复时递增;次版本 Y 在引入向后兼容的新功能、或标记公共 API 弃用时递增;主版本 X 在任何不兼容变更时递增。
  • 每次递增高段,低段 MUST 归零:1.9.1 -> 1.10.0 -> 1.11.0。
  • 版本发行后 MUST NOT 修改内容,任何改动只能发新版本。
  • 预发布版本:在修订号后接 - 和一串点分标识符。标识符只允许 ASCII 字母数字和连字符,MUST NOT 为空;纯数字标识符 MUST NOT 带前导零。例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7。预发布优先级低于对应的正式版。
  • 构建元数据:在正式版或预发布版后接 + 和一串点分标识符。比较优先级时 MUST 忽略构建元数据;仅构建元数据不同时视为同级。
  • 0.y.z 表示初始开发阶段,API 可能随时变更;从 1.0.0 起才承诺兼容。

优先级比较算法

把版本拆成 主、次、修订、预发布 四段,构建元数据直接丢掉。

  1. 主、次、修订按数值比较。
  2. 主次修订相同、且存在预发布时,预发布优先级低于正式版。
  3. 两个预发布比较:从左到右逐段比标识符。纯数字段按数值比,带字母或连字符的段按 ASCII 词法比。
  4. 数字标识符优先级总是低于非数字标识符。
  5. 前面全相同,字段更多的预发布优先级更高。

规范给的完整链条:

1.0.0-alpha  9,顺序反过来。同一类错误还有 `1.0.0` 与 `1.0.0-alpha`:前几字符相同、后者更长,字符串比较会把正式版排在预发布之后,但规范要求正式版优先级更高。

自己写比较函数时,预发布段为空要特判:

```python
def cmp_pre(a, b):          # a、b 为点分预发布串,空串代表正式版
if a == b:
return 0
if a == "":
return 1            # 正式版 > 预发布
if b == "":
return -1
for x, y in zip(a.split("."), b.split(".")):
if x == y:
continue
if x.isdigit() and y.isdigit():
return -1 if int(x)  len(b.split("."))) - (len(a.split(".")) < len(b.split(".")))

坑二:前导零非法,解析器应拒绝而不是归一化

1.02.3、1.0.0-01、1.0.0-alpha.00 都不是合法 SemVer。正则里用 0|[1-9]\d* 而不是 \d+ 就是为了挡这类输入。官方推荐的正则:

^(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-]+)*))?$

注意 v1.0.0 不匹配这个正则,Go 生态里带 v 的写法要单独处理。

坑三:构建元数据不参与比较,但去重时会被字符串相等误伤

1.0.0+build1 和 1.0.0+build2 在优先级上是同级。如果缓存 key、制品库索引、依赖去重直接用版本字符串做键,这两个会被当成不同版本,结果是同一份 1.0.0 被构建、下载或缓存两次。做集合运算前先剥掉 + 之后的部分。

坑四:0.x 的语义会让 ^ 退化

npm 的 semver 范围:~ 只允许修订号升级,^ 允许次版本升级但不改主版本。但在 0.x 下 ^ 会退化成只允许修订升级:

  • ^0.2.3 等价于 ~0.2.3
  • ^0.0.3 只匹配精确的 0.0.3
  • ^1.2.3 不会匹配预发布版本,比如 2.0.0-beta.1 不在范围内

最后一类是「预发布默认不被范围匹配」的通用规则,不是 0.x 专属。

坑五:Go 的 v 前缀与 golang.org/x/mod/semver

Go 模块在版本号前统一加 v,形如 v1.4.2;同时在主版本升到 v2 及以上时强制语义化导入版本,模块路径必须带主版本后缀,如 example.com/mod/v2。

golang.org/x/mod/semver 只认可带 v 前缀的形式,v1 和 v1.0 会被规范化为 v1.0.0:

import (
"fmt"
"sort"

"golang.org/x/mod/semver"
)

func main() {
vs := []string{"v1.0.0-alpha.10", "v1.0.0-alpha.9", "v1.0.0", "v1.0.0-beta.2"}
sort.Slice(vs, func(i, j int) bool { return semver.Compare(vs[i], vs[j]) < 0 })
fmt.Println(vs)
// [v1.0.0-alpha.9 v1.0.0-alpha.10 v1.0.0-beta.2 v1.0.0]

fmt.Println(semver.IsValid("1.0.0"))   // false,缺 v 前缀
fmt.Println(semver.Max("v1.2.3", "v1.10.0")) // v1.10.0
}

写 go.mod 时 require 必须带 v,require example.com/mod v1.2.3 合法,require example.com/mod 1.2.3 不合法。

落地检查

拿一串真实版本跑一遍排序,确认 1.0.0-alpha.9 < 1.0.0-alpha.10 < 1.0.0;对每个输入先过正则再过比较函数;比较函数里构建元数据一律剔除。这三步能挡掉上面大部分问题。

复制全文 生成海报 语义化版本 SemVer 版本号 依赖管理 Go

推荐文章

程序员茄子在线接单