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. 支持按組名稱提取匹配結果
^(?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+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 解決問題的方式是,把 pkg3v1.0.0 版本和 v2.0.0 版本當作兩個不同的包,這樣也就允許了 app 包能夠同時包含多個不同版本的 pkg3

在使用時,需要在包的導入路徑上加上包的主版本號。這裏以 go-micro 包使用爲例,展示下 Go Module 語義導入版本的用法:

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 版本。

聯繫我

微信:jianghushinian

郵箱:jianghushinian007@outlook.com

博客地址:https://jianghushinian.cn

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rFT7JTPS7mNr-3xgcu_Ikw