Go項目中使用Git Submodule,還有這個必要嗎?
在軟件開發中,依賴管理一直是一個重要的議題,特別是在像Go這樣的編程語言中,隨著項目的擴展,如何有效管理依賴變得至關重要。Git Submodule作為Git的一個重要功能,允許在一個Git倉庫中嵌入另一個倉庫,從而方便地管理跨項目的代碼共享。然而,Go語言引入的Go Module機制似乎已經解決了依賴管理的問題,那么在Go項目中,是否還有使用Git Submodule的必要呢?本文將簡單探討一下Go項目中Git Submodule的使用方法,并分析它是否還值得使用。
1. Git Submodule是什么?
Git Submodule是Git版本管理工具提供的一個功能,允許你將一個Git倉庫作為另一個Git倉庫(主倉庫)的子目錄。主倉庫通過記錄Submodule的URL和commit hash來追蹤Submodule。當你克隆一個包含Submodule的倉庫時,需要額外的步驟來初始化和更新Submodule。
下面是一個將github.com/rsc/pdf倉庫作為git submodule的示例。
我們先建立主倉庫:
$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在主倉庫的頂層目錄下創建一個.gitmodules文件:
$cat .gitmodules
[submodule "pdf"]
path = pdf
url = https://github.com/rsc/pdf.git
pdf子目錄下的.git不再是目錄而是一個文件,其內容指示了pdf倉庫的git元數據目錄的位置,即主倉庫下的.git/modules/pdf下:
$cat pdf/.git
gitdir: ../.git/modules/pdf
git submodule這種機制的主要用途是當多個項目之間有共享代碼時,避免將共享的代碼直接復制到每個項目中,而是通過Submodule來引用外部倉庫。這種方式使得共享代碼的版本控制更加明確和獨立,也方便了項目之間的更新、管理與版本控制。
通過git submodule status可以查看主倉庫下各個submodule的當前狀態:
$git submodule status
c47d69cf462f804ff58ca63c61a8fb2aed76587e pdf (v0.1.0-1-gc47d69c)
通過git submodule update還可以更新各個submodule到最新版本。但通常在主倉庫中會鎖定Submodule的特定版本,通過鎖定Submodule的版本,可以確保主倉庫使用的是經過測試和驗證的Submodule代碼,這減少了因Submodule更新而導致的意外問題。同時,鎖定版本還可以確保所有開發者和構建環境都使用完全相同版本的Submodule,這對于保證構建的一致性和可重現性至關重要。版本鎖定讓你還可以精確控制何時更新Submodule,你可以在準備好處理潛在的變更和進行必要的測試時,有計劃地更新Submodule版本。submodule的版本鎖定可以通過下面命令組合實現:
cd path/to/submodule
git checkout <specific-commit-hash>
cd -
git add path/to/submodule
git commit -m "Lock submodule to specific version"
這個提交會更新主倉庫中記錄的Submodule版本,其他克隆主倉庫的人在初始化和更新Submodule時,就會自動獲取到這個特定版本。
在以Git為版本管理工具的項目中,Submodule在以下一些場景中還是很有用的:
- 在多項目依賴場景下,我們可以使用Submodule共享公共庫;
- 在大型單一倉庫中,Submodule有助于我們模塊化管理各個子項目;
- 統一對Submodule的版本進行嚴格管理,避免在更新時引入未測試的新代碼。
submodule雖然可以解決一些問題,但由于增加了項目管理復雜度以及學習成本,應用算不上廣泛,但也不乏一些知名的開源項目在使用,比如git項目自身、openssl、qemu等。
不過,對于Go項目而言,Go Modules是Go在Go 1.11引入的新的官方依賴管理機制,它通過go.mod文件聲明依賴關系,通過go.sum文件確保依賴的完整性,實現了構建的可重現性。那么,在Go項目中還有必要引入sub modules嗎?
這里我們先不下結論,而是先來看看Go項目引入submodule后該如何使用呢。
2. Go項目的Git Submodule使用方法
在前面我們在本地建立了一個main-project,然后將rsc/pdf作為submodule導入到了main-project中,main-project是一個Go項目,它的go.mod如下:
// main-project/go.mod
module main-project
go 1.23.0
我們現在就繼續使用這個示例來看看Go項目中git submodule的使用方法。
我們先來看一種錯誤的使用方法:使用相對路徑。
我們在main-project下建立一個main.go的源文件:
// main-project/main.go
package main
import (
_ "./pdf"
)
func main() {
println("ok")
}
建完后,整個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中,我們期望使用相對路徑來導入submomdule中的pdf包,運行main.go,我們得到下面結果:
$go run main.go
main.go:4:2: "./pdf" is relative, but relative import paths are not supported in module mode
我們看到:在go module構建模式下,Go已經不再支持以相對路徑導入Go包了!但是如果我們直接通過rsc.io/pdf這個路徑導入,那顯然使用的就不是submodule中的pdf包了。
下面我們試試第二種方法,即將pdf目錄看成main-project的子目錄,將pdf包看成是main-project這個module下的一個包,這樣pdf包在main-project這個module下的導入路徑就變成了main-project/pdf:
// main-project/main.go
package main
import (
_ "main-project/pdf"
)
func main() {
println("ok")
}
這次構建和運行main.go,我們將得到正確的預期結果。
到這里,我們似乎又找到了go module之外go項目依賴管理的新方法,并且這種方法特別適合當某些依賴項目尚未發布,還無法直接通過Go Module導入的庫,甚至是一些永遠不會發布的內部庫或私有庫。這種方法讓pdf看起來是main-project的一部分,但實際上pdf包的版本卻是需要開發人員自己通過git submodule命令管理的,pdf包的版本無法用go.mod(和go.sum)控制,因為它被視為是main-project的一部分了,而不是外部依賴包。
如果你不想將其視為main-project的一部分,還想將其以外部依賴的方式管理起來,那就需要利用到go module的replace或go.work了。不過這種方法的前提是submodule下必須是一個go module,即有自己的go.mod。rsc.io/pdf包是一個legacy package,還沒有自己的go.mod,我們先在本地pdf目錄下為其添加一個go.mod:go mod init rsc.io/pdf。
接下來,我們先來簡單看看用replace如何實現導入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,這樣修改后,我們運行main.go也會得到正確的結果。
另外我們還可以使用go.work來導入pdf,下面命令初始化一個go.work:
$go work init .
編輯go.work,添加workspace包含的路徑:
go 1.23.0
use (
.
./pdf
)
這樣go編譯器會默認在當前目錄和pdf目錄下搜索rsc.io/pdf模塊,運行main.go也是ok的。
相對于將pdf包看成是main-project module下的一個包并用main-project/pdf這個內部依賴的包導入路徑的方法,使用replace或go.work的好處在于一旦pdf包得以發布,main.go可以無需修改pdf包導入路徑,并可以基于go.mod精確管理pdf包的版本。
3. 小結
那么我們在Go項目中到底是否有必要使用sub modules呢?我們來小結一下。
總的來說,在大多數情況下,Go Modules確實已經覆蓋了Git Submodule在Go項目中的主要功能,甚至做的更好,比如:Go Modules提供了更細粒度的版本控制,能自動解析和下載依賴,并也可以確保了構建的可重現性。因此,對于大多數Go項目而言,使用Go Modules已經足夠滿足依賴管理需求,而無需再使用git submodule。并且,在Go項目以及Go社區的實踐中,應對類似共享未發布的依賴包的場景(git submodule適用的場景),使用replace或go.work是比較主流的實踐,或者說go.work以及replace就是為了這種情況而添加的。
當然如果組織/公司內部尚未構建可以很好地支持內部Go項目間依賴包獲取、導入和管理的基礎設施,那么git submodule不失為一種可以在內部Go項目中實施的可行的依賴版本管理和控制方案。
最后,無論選擇使用Git Submodule、Go Modules,還是兩者結合,最重要的是要確保項目結構清晰,依賴關系明確,以便于團隊協作和項目維護。