Go modules基礎精進,六大核心概念全解析

點擊一鍵訂閱《雲薦大咖》專欄,獲取官方推薦精品內容,學技術不迷路!

 

               


Go 語言做開發時,路徑是如何定義的?Go Mudules又爲此帶來了哪些改變?本文將會全面介紹Go modules六大核心概念,包括了設計理念與兼容性原則等,掌握這些技術點對於管理和維護Go 模塊有重要價值。

上一篇文章中,筆者介紹瞭如何以經典的 hello world 爲例創建一個 Go module 模塊,需要說明的是一個模塊中是可以包含多個包(package)的,它們是可以被一起發佈、打包、版本化的。同時,Go Modules 也可以通過版本管理系統(githubgitlab)或者 goproxy 代理進行下載。在使用 Go Modules 之前,建議大家弄清楚息息相關的六大核心概念,以方便大家在後期的開發、使用過程中理解更加深入。

 

我們在使用 Go 語言做開發時經常會遇到像 example.com/test 或者 example.com/test/pkg/log這樣的路徑,這些路徑到底是怎麼定義的,兩者中存在什麼關係,在 Go Modules 中又扮演着怎樣的角色呢?Go Modules 的引入對已有的包又引入了哪些新的概念,它們是如何協作的?對兼容性提出了哪些新的要求呢?讓我們一起來看一下。

 

 

一:模塊路徑 (Module Path)


Go 使用 “module path” 來區分不同的 module 模塊,它在 go.mod 文件中被定義,這個文件中還包含了這個模塊編譯所需的其他依賴。如果一個目錄中包含了 go.mod 文件,那麼這個目錄就是這個 Go 模塊的根目錄了。

 

另外,還要介紹下包(package) 這個概念,它在 Go Modules 出現之前就已經存在了。Go 模塊中的  (package)”是處於同一目錄中的一些源代碼文件的集合,這些文件將被編譯在一起。包路徑(package path是模塊路徑和子目錄(模塊根目錄的相對路徑)的組合。舉個例子,在模塊“golang.org/x/net”下的 html 目錄中有個包,這個包的路徑是 “golang.org/x/net/html”

 

總結下來就是: 一個代碼倉庫可以包含多個 Go 模塊,一個 Go 模塊可以包含多個 Go 包。

 

模塊路徑是一個 Go 模塊的規範名稱,用於區分不通的模塊。同時他還是該模塊下 Go 包的路徑前綴。理論上,模塊路徑應該至少包含兩個關鍵信息:

模塊的作用 哪裏獲取該模塊

 

二:版本號與兼容性原則


版本號相當於是一個模塊的只讀快照,它可以是正式的發佈版本,也可以是預發佈版本。 每個版本都以字母 v 開頭,後跟一個語義版本,例如 v1.0.0

 

總而言之,語義版本由三個由點分隔的非負整數(主要版本、次要版本和補丁版本,從左到右)組成。 補丁版本後可以跟一個以連字符開頭的可選預發佈字符串。 預發佈字符串或補丁版本後可以跟一個以加號開頭的構建元數據字符串。 例如,v0.0.0v1.12.134v8.0.5-prev2.0.9+meta 等都是有效版本。

 

版本號中的信息代表了這個版本是否是一個穩定版,是否保持了與之前版本的兼容性。

 

  • 當維護的模塊發生了一些不兼容變更,比如修改了外部可調用的接口或者函數時,需要對主版本號進行遞增,並且將次版本號和補丁版本號置爲零。比如在模塊中移除了一個包。
  • 在模塊中添加一些新的函數或者接口,並沒有影響模塊的兼容性時,需要對次版本號進行遞增,並且將補丁版本號置爲零。
  • 當修復了一些 bug 或者進行了一些優化時,只需要對補丁版本號進行遞增就可以了,因爲這些變更不會對已經公開的接口進行變更。
  • 預發佈後綴代表了這個版本號是一個預發佈版本。預發佈版本號的排序會在正式版本號的前面。舉個例子,v1.2.3-pre 會排列在 v1.2.3 前面。
  • 元數據後綴會在版本比對中被忽略,版本控制中的代碼庫會忽略帶有構建元數據的標籤,但在 go.mod 文件中指定的版本中會保留構建元數據。如果一個模塊還沒有遷移到 Go Modules 並且主版本號是 2 或者更高,+incompatible 後綴會被添加到版本號上。

 

如果一個版本的主版本號是 0 或者它有一個預發佈版本後綴,那麼這個版本被認爲是一個不穩定版本。通常,不穩定版本不受兼容性限制的,舉個例子,v0.2.0 可能和 v0.1.0 是不兼容的,v1.5.0-beta 可能和 v1.5.0 也是不兼容的。

 

Go 可以通過 tags、分支、和 commit 哈希值來獲取模塊,即使這些命名沒有遵循這些規則。在主模塊中,go 命令會自動的將這些 revision 轉化爲符合標準的版本號,其被稱爲僞版本號(pseudo-version)。舉個例子,當執行下面的命令時:

go get -d golang.org/x/net@daa7c041

Go 會講指定的 hash daa7c041 轉化爲一個僞版本號 v0.0.0-20191109021931-daa7c04131f5。在主模塊之外需要規範版本,如果 go.mod 文件中出現像 master 這樣的非規範版本,go 命令會報錯。

 

 

 

三:僞版本號


僞版本號是一種預發佈版本號的格式,其中包含了指定的 commit hash 值。另外,對於沒有打標籤的代碼庫,也可以使用僞版本號來表明某個版本,它可以在正式發佈某個版本之前方便的進行測試。舉個例子,每個僞版本號都有三部分組成:

 

  1. 基本版本前綴(vX.0.0 或 vX.Y.Z-0),它要麼源自修訂版之前的語義版本標籤,要麼源自 vX.0.0(如果沒有此類標籤)。
  2. 時間戳 (yyyymmddhhmmss),這是創建 commit 的 UTC 時間。 在 Git 中,這是 commit 提交時間。
  3. commit 標識符 (abcdefabcdef),它是提交 commit 哈希的 12 個字符的前綴,或者在 Subversion 中,是一個用零填充的修訂號。

 

在這三個部分之下,又分爲以下多種情況

 

  • 如果之前沒有基版本,那麼諸如 vX.0.0-yyyymmddhhmmss-abcdefabcdef 這樣的僞版本號將被啓用。主版本號 X 需要匹配模塊的主版本號後綴。
  • 如果之前的基版本號是一個像 vX.Y.Z-pre 這樣的預發佈版本,那麼 vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef 將被採用。
  • 如果之前的基版本號是一個像 vX.Y.Z 這樣的正式版本,那麼 vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef 將被採用,舉個例子,如果基版本號是 v1.2.3,僞版本號可能是 v1.2.4-0.20191109021931-daa7c04131f5。
  • 基於不同的基礎版本號,多個僞版本號是有可能指向同一個 commit hash 的,在對一個低於已經存在的僞版本號打標籤時,這種情況就會發生。

 

上面介紹的這種僞版本號攜帶了兩個非常有用的信息:

1. 僞版本號會高於這些已經存在的基礎版本號,但是會低於後面生成的其他僞版本號。

2. 有相同基礎版本前綴的僞版本按時間順序排序。

 

僞版本號不需要手動指定。很多 Go 命令可以接受一個 commit hash 或者分支名,然後自動將其轉化爲一個僞版本號(或者一個標籤,如果存在的話)。例如:

go get -d example.com/mod@master
go list -m -json example.com/mod@abcd1234

 

四:主版本號後綴


 

從主版本號 2 開始,模塊路徑中必須添加一個像 /v2 這樣的一個和主版本號匹配的後綴。舉個例子如果一個模塊在版本 v1.0.0 是的路徑爲 example.com/test,那麼它在 v2.0.0 時的路徑將是 example.com/test/v2。

 

主版本號後綴遵循導入兼容規則:

 

如果一個新代碼包和老代碼包擁有同樣的導入路徑,那麼新包必須保證對老代碼包的向後兼容。

 

  • 根據定義,模塊的新主版本中的包與先前主版本中的相應包不向後兼容。 因此,從 v2 開始,包需要新的導入路徑。 這是通過向模塊路徑添加主版本後綴來實現的。 由於模塊路徑是模塊內每個包的導入路徑的前綴,因此將主版本後綴添加到模塊路徑可爲每個不兼容的版本提供不同的導入路徑。
  • 主版本 v0 或 v1 不允許使用主版本後綴。 v0 和 v1 之間的模塊路徑不需要更改,因爲 v0 版本爲不穩定,沒有兼容性保證。 此外,對於大多數模塊,v1 向後兼容最新的 v0 版本, v1 版本纔開始作爲對兼容性的承諾。
  • 這裏有一個特例,以 gopkg.in/ 開頭的模塊路徑必須始終具有主版本後綴,即使是 v0 和 v1 版本。 後綴必須以點而不是斜線開頭(例如,gopkg.in/yaml.v2)。因爲在 Go Modules 推出之前,gopkg.in 就沿用了這個規則,爲了能讓引入 gopkg.in 包的代碼能繼續導入編譯, Go 做了一些兼容性工作。
  • 主版本後綴可以讓一個模塊的多個主版本共存於同一個構建中。 這可以很好的解決鑽石依賴性問題(diamond dependency conflict) https://jlbp.dev/what-is-a-diamond-dependency-conflict。 通常,如果傳遞依賴項在兩個不同版本中需要一個模塊,則將使用更高的版本。 但是,如果兩個版本不兼容,則任何一個版本都不會滿足所有的調用者。 由於不兼容的版本必須具有不同的主版本號,因此主版本後綴具有不同的模塊路徑,這樣就不存在衝突了:具有不同後綴的模塊被視爲單獨的模塊,並且它們的包的導入路徑也是不同的。
  • 因爲很多 Go 項目在遷移到 Go 模塊之前就發佈了 v2 或更高版本的版本,所以沒有使用主要版本後綴。對於這些版本,Go 使用 +incompatible 構建標記來進行註釋(例如,v2.0.0+incompatible)。

 

五:解析包路徑到模塊路徑的流程


通常在使用“go get”時可能是指定到一個包路徑,而非模塊路徑,Go 是如何找到模塊路徑的呢?

 

go 命令會在主模塊(當前模塊)的 build list 中搜索有哪些模塊路徑匹配這個包路徑的前綴。舉個例子,如果導入的包路徑是 example.com/a/b,發現 example.com/a 是一個模塊路徑,那麼就會去檢查 example.com/a 在 b 目錄中是否包含這個包,在這個目錄中要至少存在一個 go 源碼文件纔會被認爲是一個有效的包。編譯約束(Build Constraints)在這一過程中不會被應用。 如果確實在 build list 中找到了一個模塊包含這個包,那麼這個模塊將被使用。如果沒有發現模塊能提供這個包或者發現兩個及兩個以上的模塊提供了這個包,那麼 go 命令會提示報錯。但是你可以指定 -mod=mod 來使 go 命令嘗試下載本地找不到的包,並且更新 go.mod 和 go.sum。go get 和 go mod tidy 這兩個命令會自動的做這些工作。

 

當 go 命令試圖下載一個新的代碼包時,它回去檢查 GOPROXY 環境變量,這是一個使用逗號分隔的 URL 列表,當然也支持像 direct 和 off 這樣的關鍵字。代理 URL 代表 go 將使用 GOPROXY 協議拉取模塊,direct 表示 go 需要和版本控制系統直接交互,off 不需要和外界做任何交互。另外,GOPRIVATE 和 GONOPROXY 環境變量也可以精細的控制 go 下載代碼包的策略。

 

對於 GOPROXY 列表中的每一項, go 命令回去請求模塊路徑的每一個前綴。對於請求成功的模塊,go 命令回去下載最新模塊並且檢查這個某塊是否包含請求的包。如果多個模塊包含了請求的包,擁有最長路徑的將被選擇。如果發現的模塊中沒有包含這個包,會報錯。如果沒有模塊被發現,go 命令會嘗試 GOPROXY 列表中的下一個配置項,如果最終都嘗試過沒有發現則會報錯。舉個例子,假設用戶想要去獲取 golang.org/x/net/html 這個包,之前配置的 GOPROXY 爲 https://corp.example.com,https://goproxy.io。go 命令會遵循下面的請求順序:

向 https://corp.example.com/ 發起請求 (並行):
Request for latest version of golang.org/x/net/html
Request for latest version of golang.org/x/net
Request for latest version of golang.org/x
Request for latest version of golang.org

如果 https://corp.example.com/ 上面都失敗了返回 410 或者 404 狀態碼,向 https://proxy.golang.org/ 發起請求:

Request for latest version of golang.org/x/net/html
Request for latest version of golang.org/x/net
Request for latest version of golang.org/x
Request for latest version of golang.org

當一個需要的模塊被發現後,go 命令會將這個依賴模塊的路徑和對應版本添加到主模塊的 go.mod 文件中。這樣就確保了以後在編譯該模塊時,同樣的模塊版本將被使用,保證了編譯的可重複性。如果解析的代碼包沒有被主模塊直接引用,在 go.mod 文件中添加的新依賴後會有 // indirect 註釋。

六:go.mod 文件


就像前面提到過的,模塊的定義是由一個 UTF-8 編碼的名爲 go.mod 文本文件定義的。 這個文件是按照“行”進行組織的(line-oriented)。每一行都有一個獨立的指令,有一個預留關鍵字和一些參數組成。比如:

module example.com/my/thing
go 1.17
require example.com/other/thing v1.0.2
require example.com/new/thing/v2 v2.3.4
exclude example.com/old/thing v1.2.3
replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5
retract [v1.9.0, v1.9.5]

開頭的關鍵詞可以以行的形式被歸總爲塊,就像日常所用的 imports 一樣,所以可以改成下面這樣:

require (
 example.com/new/thing/v2 v2.3.4
 example.com/old/thing v1.2.3
)

go.mod 文件的設計兼顧了開發者的可讀性和機器的易寫性。go 命令也提供了幾個子命令來幫組開發者修改 go.mod 文件。舉個例子,go get 命令可以在需要的時候更新 go.mod 文件。go mod edit 命令可以對文件做一些底層的修改操作。如果我們也有類似的需求,可以使用 golang.org/x/mod/modfile 包以編程方式進行同樣的更改。通過這個包,也可以一窺底層 go.mod 的 struct 結構:

// go.mod 文件的組成形式
type File struct {
 Module *Module  // 模塊路徑
 Go *Go  // Go 版本
 Require []*Require // 依賴模塊
 Exclude []*Exclude // 排除模塊
 Replace []*Replace // 替換模塊
 Retract []*Retract // 撤回模塊
}
// A Module is the module statement.
type Module struct {
 Mod module.Version
 Deprecated string
}
// A Go is the go statement.
type Go struct {
 Version string // "1.23"
}
// An Exclude is a single exclude statement.
type Exclude struct {
 Mod module.Version
}
// A Replace is a single replace statement.
type Replace struct {
 Old module.Version
 New module.Version
}
// A Retract is a single retract statement.
type Retract struct {
 VersionInterval
 Rationale string
}

從上面的 Module 的 struct 中可以看到 “Deprecated”這一結構,在 Go Modules 推出的早期是沒有這個設計的,那麼這個字段是做什麼用的呢? 估計很多人都不知道,如果我們維護的一個模塊主版本從 v1 演進到了 v2,而不再維護 v1 版本了,希望用戶儘可能使用 v2,通過上面的介紹知道v1 和 v2 是不同的 import path,“Retract”也無能爲力,這時候這個 “Deprecated”就起作用了,看下面的例子:

// Deprecated: in example.com/a/[email protected], the latest supported version is example.com/a/b/v2.
module example.com/a/b
go 1.17

當用戶再去獲取 example.com/a/b 這個版本時,go 命令可以感知到這個版本已經不再維護了,會報告給用戶:

go get -d example.com/a/b@v1.9.0
go: warning: module example.com/deprecated/a is deprecated: in example.com/a/b@v1.9.0, the latest supported version is example.com/a/b/v2

用戶就可以根據提示進行 v2 代碼拉取了。

《Go modules基礎精進,六大核心概念全解析》一文全面介紹了 Go Modules 中的模塊、模塊路徑、包、包路徑、如何通過包路徑尋找模塊路徑,還介紹了版本號和僞版本號,最後簡單介紹了 go.mod 文件,以及其中不爲人知的“Deprecated”功能,瞭解這些概念、設計理念和兼容性原則,將對管理和維護自己的 Go 模塊大有幫助。

以上這些概念都是平常使用 Go 語言會高頻接觸到的內容,理解版本號和僞版本號的區別和設計原則,可以幫助我們清楚按照 semver 的標準定義自己的 tag 是多麼重要。同時,遵循Go Modules 定義的兼容性原則,上下游開發者在社區協同時將會變得更加友好和高效。接下來的系列文章將會開始具體來了解 Go Modules 中的設計細節,例如 go.mod 文件詳解以及配套的 go mod 子命令等,敬請期待。另外,騰訊雲 goproxy 企業版已經產品化,需要了解的同學可以點擊這裏

                       

李保坤往期精彩文章推薦:Go語言重新開始,Go Modules 的前世今生與基本使用

《雲薦大咖》是騰訊雲加社區精品內容專欄。雲薦官特邀行業佼者,聚焦於前沿技術的落地及理論實踐之上,持續爲您解讀雲時代熱點技術、探索行業發展新機。點擊一鍵訂閱,我們將爲你定期推送精品內容。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章