Go項(xiàng)目中使用Git Submodule,還有這個(gè)必要嗎?
在軟件開發(fā)中,依賴管理一直是一個(gè)重要的議題,特別是在像Go這樣的編程語言中,隨著項(xiàng)目的擴(kuò)展,如何有效管理依賴變得至關(guān)重要。Git Submodule作為Git的一個(gè)重要功能,允許在一個(gè)Git倉(cāng)庫(kù)中嵌入另一個(gè)倉(cāng)庫(kù),從而方便地管理跨項(xiàng)目的代碼共享。然而,Go語言引入的Go Module機(jī)制似乎已經(jīng)解決了依賴管理的問題,那么在Go項(xiàng)目中,是否還有使用Git Submodule的必要呢?本文將簡(jiǎn)單探討一下Go項(xiàng)目中Git Submodule的使用方法,并分析它是否還值得使用。
1. Git Submodule是什么?
Git Submodule是Git版本管理工具提供的一個(gè)功能,允許你將一個(gè)Git倉(cāng)庫(kù)作為另一個(gè)Git倉(cāng)庫(kù)(主倉(cāng)庫(kù))的子目錄。主倉(cāng)庫(kù)通過記錄Submodule的URL和commit hash來追蹤Submodule。當(dāng)你克隆一個(gè)包含Submodule的倉(cāng)庫(kù)時(shí),需要額外的步驟來初始化和更新Submodule。
下面是一個(gè)將github.com/rsc/pdf倉(cāng)庫(kù)作為git submodule的示例。
我們先建立主倉(cāng)庫(kù):
$mkdir main-project
$cd main-project
$go mod init main-project
$git init
$git add -A
$git commit -m"initial import" .
[master (root-commit) 8227e65] initial import
1 file changed, 3 insertions(+)
create mode 100644 go.mod
接下來,我們來添加submodule:
$git submodule add https://github.com/rsc/pdf.git
Cloning into '/Users/tonybai/Test/Go/submodule/main-project/pdf'...
remote: Enumerating objects: 48, done.
remote: Counting objects: 100% (30/30), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 48 (delta 21), reused 21 (delta 21), pack-reused 18 (from 1)
Unpacking objects: 100% (48/48), done.
$git commit -m "Add rsc/pdf as a submodule"
[master 2778170] Add rsc/pdf as a submodule
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 pdf
git submodule在主倉(cāng)庫(kù)的頂層目錄下創(chuàng)建一個(gè).gitmodules文件:
$cat .gitmodules
[submodule "pdf"]
path = pdf
url = https://github.com/rsc/pdf.git
pdf子目錄下的.git不再是目錄而是一個(gè)文件,其內(nèi)容指示了pdf倉(cāng)庫(kù)的git元數(shù)據(jù)目錄的位置,即主倉(cāng)庫(kù)下的.git/modules/pdf下:
$cat pdf/.git
gitdir: ../.git/modules/pdf
git submodule這種機(jī)制的主要用途是當(dāng)多個(gè)項(xiàng)目之間有共享代碼時(shí),避免將共享的代碼直接復(fù)制到每個(gè)項(xiàng)目中,而是通過Submodule來引用外部倉(cāng)庫(kù)。這種方式使得共享代碼的版本控制更加明確和獨(dú)立,也方便了項(xiàng)目之間的更新、管理與版本控制。
通過git submodule status可以查看主倉(cāng)庫(kù)下各個(gè)submodule的當(dāng)前狀態(tài):
$git submodule status
c47d69cf462f804ff58ca63c61a8fb2aed76587e pdf (v0.1.0-1-gc47d69c)
通過git submodule update還可以更新各個(gè)submodule到最新版本。但通常在主倉(cāng)庫(kù)中會(huì)鎖定Submodule的特定版本,通過鎖定Submodule的版本,可以確保主倉(cāng)庫(kù)使用的是經(jīng)過測(cè)試和驗(yàn)證的Submodule代碼,這減少了因Submodule更新而導(dǎo)致的意外問題。同時(shí),鎖定版本還可以確保所有開發(fā)者和構(gòu)建環(huán)境都使用完全相同版本的Submodule,這對(duì)于保證構(gòu)建的一致性和可重現(xiàn)性至關(guān)重要。版本鎖定讓你還可以精確控制何時(shí)更新Submodule,你可以在準(zhǔn)備好處理潛在的變更和進(jìn)行必要的測(cè)試時(shí),有計(jì)劃地更新Submodule版本。submodule的版本鎖定可以通過下面命令組合實(shí)現(xiàn):
cd path/to/submodule
git checkout <specific-commit-hash>
cd -
git add path/to/submodule
git commit -m "Lock submodule to specific version"
這個(gè)提交會(huì)更新主倉(cāng)庫(kù)中記錄的Submodule版本,其他克隆主倉(cāng)庫(kù)的人在初始化和更新Submodule時(shí),就會(huì)自動(dòng)獲取到這個(gè)特定版本。
在以Git為版本管理工具的項(xiàng)目中,Submodule在以下一些場(chǎng)景中還是很有用的:
- 在多項(xiàng)目依賴場(chǎng)景下,我們可以使用Submodule共享公共庫(kù);
- 在大型單一倉(cāng)庫(kù)中,Submodule有助于我們模塊化管理各個(gè)子項(xiàng)目;
- 統(tǒng)一對(duì)Submodule的版本進(jìn)行嚴(yán)格管理,避免在更新時(shí)引入未測(cè)試的新代碼。
submodule雖然可以解決一些問題,但由于增加了項(xiàng)目管理復(fù)雜度以及學(xué)習(xí)成本,應(yīng)用算不上廣泛,但也不乏一些知名的開源項(xiàng)目在使用,比如git項(xiàng)目自身、openssl、qemu等。
不過,對(duì)于Go項(xiàng)目而言,Go Modules是Go在Go 1.11引入的新的官方依賴管理機(jī)制,它通過go.mod文件聲明依賴關(guān)系,通過go.sum文件確保依賴的完整性,實(shí)現(xiàn)了構(gòu)建的可重現(xiàn)性。那么,在Go項(xiàng)目中還有必要引入sub modules嗎?
這里我們先不下結(jié)論,而是先來看看Go項(xiàng)目引入submodule后該如何使用呢。
2. Go項(xiàng)目的Git Submodule使用方法
在前面我們?cè)诒镜亟⒘艘粋€(gè)main-project,然后將rsc/pdf作為submodule導(dǎo)入到了main-project中,main-project是一個(gè)Go項(xiàng)目,它的go.mod如下:
// main-project/go.mod
module main-project
go 1.23.0
我們現(xiàn)在就繼續(xù)使用這個(gè)示例來看看Go項(xiàng)目中g(shù)it submodule的使用方法。
我們先來看一種錯(cuò)誤的使用方法:使用相對(duì)路徑。
我們?cè)趍ain-project下建立一個(gè)main.go的源文件:
// main-project/main.go
package main
import (
_ "./pdf"
)
func main() {
println("ok")
}
建完后,整個(gè)main-project的目錄布局如下:
$tree -F
.
├── go.mod
├── main.go
└── pdf/
├── LICENSE
├── README.md
├── lex.go
├── name.go
├── page.go
├── pdfpasswd/
│ └── main.go
├── ps.go
├── read.go
└── text.go
在第一版main.go中,我們期望使用相對(duì)路徑來導(dǎo)入submomdule中的pdf包,運(yùn)行main.go,我們得到下面結(jié)果:
$go run main.go
main.go:4:2: "./pdf" is relative, but relative import paths are not supported in module mode
我們看到:在go module構(gòu)建模式下,Go已經(jīng)不再支持以相對(duì)路徑導(dǎo)入Go包了!但是如果我們直接通過rsc.io/pdf這個(gè)路徑導(dǎo)入,那顯然使用的就不是submodule中的pdf包了。
下面我們?cè)囋?strong>第二種方法,即將pdf目錄看成main-project的子目錄,將pdf包看成是main-project這個(gè)module下的一個(gè)包,這樣pdf包在main-project這個(gè)module下的導(dǎo)入路徑就變成了main-project/pdf:
// main-project/main.go
package main
import (
_ "main-project/pdf"
)
func main() {
println("ok")
}
這次構(gòu)建和運(yùn)行main.go,我們將得到正確的預(yù)期結(jié)果。
到這里,我們似乎又找到了go module之外go項(xiàng)目依賴管理的新方法,并且這種方法特別適合當(dāng)某些依賴項(xiàng)目尚未發(fā)布,還無法直接通過Go Module導(dǎo)入的庫(kù),甚至是一些永遠(yuǎn)不會(huì)發(fā)布的內(nèi)部庫(kù)或私有庫(kù)。這種方法讓pdf看起來是main-project的一部分,但實(shí)際上pdf包的版本卻是需要開發(fā)人員自己通過git submodule命令管理的,pdf包的版本無法用go.mod(和go.sum)控制,因?yàn)?strong>它被視為是main-project的一部分了,而不是外部依賴包。
如果你不想將其視為main-project的一部分,還想將其以外部依賴的方式管理起來,那就需要利用到go module的replace或go.work了。不過這種方法的前提是submodule下必須是一個(gè)go module,即有自己的go.mod。rsc.io/pdf包是一個(gè)legacy package,還沒有自己的go.mod,我們先在本地pdf目錄下為其添加一個(gè)go.mod:go mod init rsc.io/pdf。
接下來,我們先來簡(jiǎn)單看看用replace如何實(shí)現(xiàn)導(dǎo)入pdf包,我們需要修改一下main-project/go.mod:
// main-project/go.mod
module main-project
go 1.23.0
require rsc.io/pdf v0.1.1
replace rsc.io/pdf => ./pdf
這里我們用replace指示符將rsc.io/pdf替換為本地pdf目錄下的go module,這樣修改后,我們運(yùn)行main.go也會(huì)得到正確的結(jié)果。
另外我們還可以使用go.work來導(dǎo)入pdf,下面命令初始化一個(gè)go.work:
$go work init .
編輯go.work,添加workspace包含的路徑:
go 1.23.0
use (
.
./pdf
)
這樣go編譯器會(huì)默認(rèn)在當(dāng)前目錄和pdf目錄下搜索rsc.io/pdf模塊,運(yùn)行main.go也是ok的。
相對(duì)于將pdf包看成是main-project module下的一個(gè)包并用main-project/pdf這個(gè)內(nèi)部依賴的包導(dǎo)入路徑的方法,使用replace或go.work的好處在于一旦pdf包得以發(fā)布,main.go可以無需修改pdf包導(dǎo)入路徑,并可以基于go.mod精確管理pdf包的版本。
3. 小結(jié)
那么我們?cè)贕o項(xiàng)目中到底是否有必要使用sub modules呢?我們來小結(jié)一下。
總的來說,在大多數(shù)情況下,Go Modules確實(shí)已經(jīng)覆蓋了Git Submodule在Go項(xiàng)目中的主要功能,甚至做的更好,比如:Go Modules提供了更細(xì)粒度的版本控制,能自動(dòng)解析和下載依賴,并也可以確保了構(gòu)建的可重現(xiàn)性。因此,對(duì)于大多數(shù)Go項(xiàng)目而言,使用Go Modules已經(jīng)足夠滿足依賴管理需求,而無需再使用git submodule。并且,在Go項(xiàng)目以及Go社區(qū)的實(shí)踐中,應(yīng)對(duì)類似共享未發(fā)布的依賴包的場(chǎng)景(git submodule適用的場(chǎng)景),使用replace或go.work是比較主流的實(shí)踐,或者說go.work以及replace就是為了這種情況而添加的。
當(dāng)然如果組織/公司內(nèi)部尚未構(gòu)建可以很好地支持內(nèi)部Go項(xiàng)目間依賴包獲取、導(dǎo)入和管理的基礎(chǔ)設(shè)施,那么git submodule不失為一種可以在內(nèi)部Go項(xiàng)目中實(shí)施的可行的依賴版本管理和控制方案。
最后,無論選擇使用Git Submodule、Go Modules,還是兩者結(jié)合,最重要的是要確保項(xiàng)目結(jié)構(gòu)清晰,依賴關(guān)系明確,以便于團(tuán)隊(duì)協(xié)作和項(xiàng)目維護(hù)。