Go modules基礎(chǔ)精進,六大核心概念全解析(下)

在上篇中,你了解了模塊路徑、版本號與兼容性原則、偽版本號三大概念,那么關(guān)于Go Modules的核心概念還有什么值得一看呢?讓我們走進下篇精彩內(nèi)容!

在上篇中,我們介紹了模塊路徑、版本號與兼容性原則、偽版本號三大概念,而在下篇我們將會繼續(xù)介紹Go Modules核心概念。
四:主版本號后綴
從主版本號 2 開始,模塊路徑中必須添加一個像 /v2 這樣的一個和主版本號匹配的后綴。舉個例子如果一個模塊在版本 v1.0.0 是的路徑為 example.com/test,那么它在 v2.0.0 時的路徑將是 example.com/test/v2。
主版本號后綴遵循導(dǎo)入兼容規(guī)則:
如果一個新代碼包和老代碼包擁有同樣的導(dǎo)入路徑,那么新包必須保證對老代碼包的向后兼容。
根據(jù)定義,模塊的新主版本中的包與先前主版本中的相應(yīng)包不向后兼容。因此,從 v2 開始,包需要新的導(dǎo)入路徑。這是通過向模塊路徑添加主版本后綴來實現(xiàn)的。由于模塊路徑是模塊內(nèi)每個包的導(dǎo)入路徑的前綴,因此將主版本后綴添加到模塊路徑可為每個不兼容的版本提供不同的導(dǎo)入路徑。
主版本 v0 或 v1 不允許使用主版本后綴。v0 和 v1 之間的模塊路徑不需要更改,因為 v0 版本為不穩(wěn)定,沒有兼容性保證。此外,對于大多數(shù)模塊,v1 向后兼容最新的 v0 版本, v1 版本才開始作為對兼容性的承諾。
這里有一個特例,以 gopkg.in/ 開頭的模塊路徑必須始終具有主版本后綴,即使是 v0 和 v1 版本。后綴必須以點而不是斜線開頭(例如,gopkg.in/yaml.v2)。因為在 Go Modules 推出之前,gopkg.in 就沿用了這個規(guī)則,為了能讓引入 gopkg.in 包的代碼能繼續(xù)導(dǎo)入編譯, Go 做了一些兼容性工作。
主版本后綴可以讓一個模塊的多個主版本共存于同一個構(gòu)建中。這可以很好的解決鉆石依賴性問題(diamond dependency conflict) https://jlbp.dev/what-is-a-diamond-dependency-conflict。通常,如果傳遞依賴項在兩個不同版本中需要一個模塊,則將使用更高的版本。但是,如果兩個版本不兼容,則任何一個版本都不會滿足所有的調(diào)用者。由于不兼容的版本必須具有不同的主版本號,因此主版本后綴具有不同的模塊路徑,這樣就不存在沖突了:具有不同后綴的模塊被視為單獨的模塊,并且它們的包的導(dǎo)入路徑也是不同的。
因為很多 Go 項目在遷移到 Go 模塊之前就發(fā)布了 v2 或更高版本的版本,所以沒有使用主要版本后綴。對于這些版本,Go 使用 +incompatible 構(gòu)建標(biāo)記來進行注釋(例如,v2.0.0+incompatible)。
五:解析包路徑到模塊路徑的流程
通常在使用“go get”時可能是指定到一個包路徑,而非模塊路徑,Go 是如何找到模塊路徑的呢?
go 命令會在主模塊(當(dāng)前模塊)的 build list 中搜索有哪些模塊路徑匹配這個包路徑的前綴。舉個例子,如果導(dǎo)入的包路徑是 example.com/a/b,發(fā)現(xiàn) example.com/a 是一個模塊路徑,那么就會去檢查 example.com/a 在 b 目錄中是否包含這個包,在這個目錄中要至少存在一個 go 源碼文件才會被認為是一個有效的包。編譯約束(Build Constraints)在這一過程中不會被應(yīng)用。如果確實在 build list 中找到了一個模塊包含這個包,那么這個模塊將被使用。如果沒有發(fā)現(xiàn)模塊能提供這個包或者發(fā)現(xiàn)兩個及兩個以上的模塊提供了這個包,那么 go 命令會提示報錯。但是你可以指定 -mod=mod 來使 go 命令嘗試下載本地找不到的包,并且更新 go.mod 和 go.sum。go get 和 go mod tidy 這兩個命令會自動的做這些工作。
當(dāng) go 命令試圖下載一個新的代碼包時,它回去檢查 GOPROXY 環(huán)境變量,這是一個使用逗號分隔的 URL 列表,當(dāng)然也支持像 direct 和 off 這樣的關(guān)鍵字。代理 URL 代表 go 將使用 GOPROXY 協(xié)議拉取模塊,direct 表示 go 需要和版本控制系統(tǒng)直接交互,off 不需要和外界做任何交互。另外,GOPRIVATE 和 GONOPROXY 環(huán)境變量也可以精細的控制 go 下載代碼包的策略。
對于 GOPROXY 列表中的每一項, go 命令回去請求模塊路徑的每一個前綴。對于請求成功的模塊,go 命令回去下載最新模塊并且檢查這個某塊是否包含請求的包。如果多個模塊包含了請求的包,擁有最長路徑的將被選擇。如果發(fā)現(xiàn)的模塊中沒有包含這個包,會報錯。如果沒有模塊被發(fā)現(xiàn),go 命令會嘗試 GOPROXY 列表中的下一個配置項,如果最終都嘗試過沒有發(fā)現(xiàn)則會報錯。舉個例子,假設(shè)用戶想要去獲取 golang.org/x/net/html 這個包,之前配置的 GOPROXY 為 https://corp.example.com,https://goproxy.io。go 命令會遵循下面的請求順序:
向?https://corp.example.com/?發(fā)起請求?(并行):
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 狀態(tài)碼,向 https://proxy.golang.org/ 發(fā)起請求:
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
當(dāng)一個需要的模塊被發(fā)現(xiàn)后,go 命令會將這個依賴模塊的路徑和對應(yīng)版本添加到主模塊的 go.mod 文件中。這樣就確保了以后在編譯該模塊時,同樣的模塊版本將被使用,保證了編譯的可重復(fù)性。如果解析的代碼包沒有被主模塊直接引用,在 go.mod 文件中添加的新依賴后會有 // indirect 注釋。
六:go.mod 文件
就像前面提到過的,模塊的定義是由一個 UTF-8 編碼的名為 go.mod 文本文件定義的。這個文件是按照“行”進行組織的(line-oriented)。每一行都有一個獨立的指令,有一個預(yù)留關(guān)鍵字和一些參數(shù)組成。比如:
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]
開頭的關(guān)鍵詞可以以行的形式被歸總為塊,就像日常所用的 imports 一樣,所以可以改成下面這樣:
require?(
?example.com/new/thing/v2?v2.3.4
?example.com/old/thing?v1.2.3
)
go.mod 文件的設(shè)計兼顧了開發(fā)者的可讀性和機器的易寫性。go 命令也提供了幾個子命令來幫組開發(fā)者修改 go.mod 文件。舉個例子,go get 命令可以在需要的時候更新 go.mod 文件。go mod edit 命令可以對文件做一些底層的修改操作。如果我們也有類似的需求,可以使用 golang.org/x/mod/modfile 包以編程方式進行同樣的更改。通過這個包,也可以一窺底層 go.mod 的 struct 結(jié)構(gòu):
//?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”這一結(jié)構(gòu),在 Go Modules 推出的早期是沒有這個設(shè)計的,那么這個字段是做什么用的呢?估計很多人都不知道,如果我們維護的一個模塊主版本從 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
當(dāng)用戶再去獲取 example.com/a/b 這個版本時,go 命令可以感知到這個版本已經(jīng)不再維護了,會報告給用戶:
go?get?-d?example.com/a/[email protected]
go:?warning:?module?example.com/deprecated/a?is?deprecated:?in?example.com/a/[email protected],?the?latest?supported?version?is?example.com/a/b/v2
用戶就可以根據(jù)提示進行 v2 代碼拉取了。
《Go modules基礎(chǔ)精進,六大核心概念全解析》一文全面介紹了 Go Modules 中的模塊、模塊路徑、包、包路徑、如何通過包路徑尋找模塊路徑,還介紹了版本號和偽版本號,最后簡單介紹了 go.mod 文件,以及其中不為人知的“Deprecated”功能,了解這些概念、設(shè)計理念和兼容性原則,將對管理和維護自己的 Go 模塊大有幫助。
以上這些概念都是平常使用 Go 語言會高頻接觸到的內(nèi)容,理解版本號和偽版本號的區(qū)別和設(shè)計原則,可以幫助我們清楚按照 semver 的標(biāo)準(zhǔn)定義自己的 tag 是多么重要。同時,遵循Go Modules 定義的兼容性原則,上下游開發(fā)者在社區(qū)協(xié)同時將會變得更加友好和高效。接下來的系列文章將會開始具體來了解 Go Modules 中的設(shè)計細節(jié),例如 go.mod 文件詳解以及配套的 go mod 子命令等,敬請期待。另外,騰訊云 goproxy 企業(yè)版已經(jīng)產(chǎn)品化,需要了解的同學(xué)可以點擊這里(https://goproxy.io/zh/docs/enterprise.html)。

想要了解更多有關(guān) Go 語言的資訊動態(tài),還可通過掃描下方二維碼,進去一起探討交流哦~
