深入理解Go中的項(xiàng)目代碼布局
一、Go 語(yǔ)言“創(chuàng)世項(xiàng)目”結(jié)構(gòu)
Go 語(yǔ)言的創(chuàng)世項(xiàng)目其實(shí)就是 Go 語(yǔ)言項(xiàng)目自身,是全世界第一個(gè) Go 語(yǔ)言項(xiàng)目。
Go 1.5 版本實(shí)現(xiàn)自舉前,C 語(yǔ)言代碼行數(shù)也占據(jù)了 32.10%,在之后實(shí)現(xiàn)版本自舉后,Go 語(yǔ)言代碼行數(shù)占比將近 90%,C 語(yǔ)言比例下降為不到 1%。
在這個(gè)版本迭代過(guò)程中,Go 語(yǔ)言項(xiàng)目的布局結(jié)構(gòu)卻整體保留了下來(lái)。
Go 語(yǔ)言項(xiàng)目結(jié)構(gòu)布局對(duì)后續(xù) Go 社區(qū)的項(xiàng)目具有重要的參考價(jià)值,尤其是 Go 項(xiàng)目早期 src 目錄下面的結(jié)構(gòu)。
首先,我們從GitHub下載Go語(yǔ)言的源代碼:
git clone http://github.com/golang/go.git
在進(jìn)入 Go 語(yǔ)言項(xiàng)目的根目錄后,我們可以使用 “tree” 命令來(lái)查看該項(xiàng)目的初始源代碼結(jié)構(gòu)布局。以 Go 1.3 版本為例,查看結(jié)果如下所示:
$cd go // 進(jìn)入Go語(yǔ)言項(xiàng)目根目錄
$git checkout go1.3 // 切換到go 1.3版本
$tree -LF 1 ./src // 查看src目錄下的結(jié)構(gòu)布局
./src
├── all.bash*
├── clean.bash*
├── cmd/
├── make.bash*
├── Make.dist
├── pkg/
├── race.bash*
├── run.bash*
... ...
└── sudo.bash*
1.1 src 目錄結(jié)構(gòu)三個(gè)特點(diǎn)
從上面的結(jié)果來(lái)看,src 目錄下面的結(jié)構(gòu)有這三個(gè)特點(diǎn)
1.**頂層腳本文件:**以 all.bash 為代表的代碼構(gòu)建的腳本源文件放在了 src 下面的頂層目錄下
2.可執(zhí)行文件目錄(cmd): src 下的二級(jí)目錄 cmd 下面存放著 Go 相關(guān)可執(zhí)行文件的相關(guān)目錄,我們可以深入查看一下 cmd 目錄下的結(jié)構(gòu):
cd cmd
tree .
# 看到如下結(jié)果
./cmd
... ...
├── 6a/
├── 6c/
├── 6g/
... ...
├── cc/
├── cgo/
├── dist/
├── fix/
├── gc/
├── go/
├── gofmt/
├── ld/
├── nm/
├── objdump/
├── pack/
└── yacc/
可以看到,這里的每個(gè)子目錄都是一個(gè) Go 工具鏈命令或子命令對(duì)應(yīng)的可執(zhí)行文件。其中,6a、6c、6g 等是早期 Go 版本針對(duì)特定平臺(tái)的匯編器、編譯器等的特殊命名方式。
3.**標(biāo)準(zhǔn)庫(kù)和運(yùn)行時(shí)實(shí)現(xiàn)(pkg):**你會(huì)看到 src 下的二級(jí)目錄 pkg 下面存放著運(yùn)行時(shí)實(shí)現(xiàn)、標(biāo)準(zhǔn)庫(kù)包實(shí)現(xiàn),這些包既可以被上面 cmd 下各程序所導(dǎo)入,也可以被 Go 語(yǔ)言項(xiàng)目之外的 Go 程序依賴并導(dǎo)入。下面是我們通過(guò) tree 命令查看 pkg 下面結(jié)構(gòu)的輸出結(jié)果:
cd pkg
tree .
# 看到如下結(jié)果
./pkg
... ...
├── flag/
├── fmt/
├── go/
├── hash/
├── html/
├── image/
├── index/
├── io/
... ...
├── net/
├── os/
├── path/
├── reflect/
├── regexp/
├── runtime/
├── sort/
├── strconv/
├── strings/
├── sync/
├── syscall/
├── testing/
├── text/
├── time/
├── unicode/
└── unsafe/
這種源代碼結(jié)構(gòu)布局風(fēng)格對(duì)后續(xù)許多 Go 項(xiàng)目的布局產(chǎn)生了影響,包括一些知名項(xiàng)目如 Go 調(diào)試器 Delve、容器技術(shù)項(xiàng)目 Docker,以及容器編排項(xiàng)目 Kubernetes,它們?nèi)匀槐3种愃频捻?xiàng)目布局風(fēng)格。這種一致性有助于開(kāi)發(fā)者更容易理解和導(dǎo)航不同 Go 項(xiàng)目的源代碼結(jié)構(gòu)。
二、Go 項(xiàng)目布局演進(jìn)
當(dāng)然,現(xiàn)在布局結(jié)構(gòu)也在一直在不斷地演化,簡(jiǎn)單來(lái)說(shuō)可以歸納為下面三個(gè)比較重要的演進(jìn)。
2.1 演進(jìn)一:Go 1.4 版本刪除 pkg 這一中間層目錄并引入 internal 目錄
Go 語(yǔ)言項(xiàng)目在其 1.4 版本中進(jìn)行了源碼樹(shù)結(jié)構(gòu)的簡(jiǎn)化和優(yōu)化,主要體現(xiàn)在以下兩個(gè)方面:
簡(jiǎn)化源碼樹(shù)層次: Go 1.4 版本刪除了原有源碼樹(shù)中的 “src/pkg/xxx” 這一層級(jí)目錄,直接使用 “src/xxx” 的結(jié)構(gòu)。這一變化減少了源碼樹(shù)的深度,使得 Go 項(xiàng)目源碼更易于閱讀和探索。
引入 internal 包機(jī)制: Go 1.4 引入 internal 包機(jī)制,增加了 internal 目錄。這個(gè) internal 機(jī)制其實(shí)是所有 Go 項(xiàng)目都可以用的,Go 語(yǔ)言項(xiàng)目自身也是自 Go 1.4 版本起,就使用 internal 機(jī)制了。根據(jù) internal 機(jī)制的定義,一個(gè) Go 項(xiàng)目里的 internal 目錄下的 Go 包,只可以被本項(xiàng)目?jī)?nèi)部的包導(dǎo)入。項(xiàng)目外部是無(wú)法導(dǎo)入這個(gè) internal 目錄下面的包的??梢哉f(shuō),internal 目錄的引入,讓一個(gè) Go 項(xiàng)目中 Go 包的分類與用途變得更加清晰。
2.2 演進(jìn)二:Go1.6 版本增加 vendor 目錄
第二次的演進(jìn),其實(shí)是為了解決 Go 包依賴版本管理的問(wèn)題,Go 核心團(tuán)隊(duì)在 Go 1.5 版本中做了第一次改進(jìn)。增加了 vendor 構(gòu)建機(jī)制,也就是 Go 源碼的編譯可以不在 GOPATH 環(huán)境變量下面搜索依賴包的路徑,而在 vendor 目錄下查找對(duì)應(yīng)的依賴包。
Go 語(yǔ)言項(xiàng)目自身也在 Go 1.6 版本中增加了 vendor 目錄以支持 vendor 構(gòu)建,但 vendor 目錄并沒(méi)有實(shí)質(zhì)性緩存任何第三方包。直到 Go 1.7 版本,Go 才真正在 vendor 下緩存了其依賴的外部包。這些依賴包主要是 golang.org/x 下面的包,這些包同樣是由 Go 核心團(tuán)隊(duì)維護(hù)的,并且其更新速度不受 Go 版本發(fā)布周期的影響。
vendor 機(jī)制與目錄的引入,讓 Go 項(xiàng)目第一次具有了可重現(xiàn)構(gòu)建(Reproducible Build)的能力。
2.3 演進(jìn)三:Go 1.13 版本引入 go.mod 和 go.sum
第三次演進(jìn),還是為了解決 Go 包依賴版本管理的問(wèn)題。在 Go 1.11 版本中,Go 核心團(tuán)隊(duì)做出了第二次改進(jìn)嘗試:引入了 Go Module 構(gòu)建機(jī)制,也就是在項(xiàng)目引入 go.mod 以及在 go.mod 中明確項(xiàng)目所依賴的第三方包和版本,項(xiàng)目的構(gòu)建就將擺脫 GOPATH 的束縛,實(shí)現(xiàn)精準(zhǔn)的可重現(xiàn)構(gòu)建。
Go 語(yǔ)言項(xiàng)目自身在 Go 1.13 版本引入 go.mod 和 go.sum 以支持 Go Module 構(gòu)建機(jī)制,下面是 Go 1.13 版本的 go.mod 文件內(nèi)容:
module std go 1.13 require ( golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 golang.org/x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect golang.org/x/text v0.3.2 // indirect )
我們看到,Go 語(yǔ)言項(xiàng)目自身所依賴的包在 go.mod 中都有對(duì)應(yīng)的信息,而原本這些依賴包是緩存在 vendor 目錄下的。
總的來(lái)說(shuō),這三次演進(jìn)主要體現(xiàn)在簡(jiǎn)化結(jié)構(gòu)布局,以及優(yōu)化包依賴管理方面,起到了改善 Go 開(kāi)發(fā)體驗(yàn)的作用。可以說(shuō),Go 創(chuàng)世項(xiàng)目的源碼布局以及演化對(duì) Go 社區(qū)項(xiàng)目的布局具有重要的啟發(fā)意義,以至于在多年的 Go 社區(qū)實(shí)踐后,Go 社區(qū)逐漸形成了公認(rèn)的 Go 項(xiàng)目的典型結(jié)構(gòu)布局。
三、現(xiàn)在 Go 項(xiàng)目的典型結(jié)構(gòu)布局
Go 項(xiàng)目通常分為可執(zhí)行程序項(xiàng)目和庫(kù)項(xiàng)目,現(xiàn)在我們就來(lái)分析一下這兩類 Go 項(xiàng)目的典型結(jié)構(gòu)布局分別是怎樣的。
3.1 Go 可執(zhí)行程序項(xiàng)目的典型結(jié)構(gòu)布局
可執(zhí)行程序項(xiàng)目是以構(gòu)建可執(zhí)行程序?yàn)槟康牡捻?xiàng)目,Go 社區(qū)針對(duì)這類 Go 項(xiàng)目所形成的典型結(jié)構(gòu)布局是這樣的:
$tree -F exe-layout
exe-layout
├── cmd/
│ ├── app1/
│ │ └── main.go
│ └── app2/
│ └── main.go
├── go.mod
├── go.sum
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
├── pkg2/
│ └── pkg2.go
└── vendor/
這樣的一個(gè) Go 項(xiàng)目典型布局就是“脫胎”于 Go 創(chuàng)世項(xiàng)目的最新結(jié)構(gòu)布局,我現(xiàn)在跟你解釋一下這里面的幾個(gè)要點(diǎn)。
我們從上往下按順序來(lái),先來(lái)看 cmd 目錄。cmd 目錄就是存放項(xiàng)目要編譯構(gòu)建的可執(zhí)行文件對(duì)應(yīng)的 main 包的源文件。如果你的項(xiàng)目中有多個(gè)可執(zhí)行文件需要構(gòu)建,每個(gè)可執(zhí)行文件的 main 包單獨(dú)放在一個(gè)子目錄中,比如圖中的 app1、app2,cmd 目錄下的各 app 的 main 包將整個(gè)項(xiàng)目的依賴連接在一起。
而且通常來(lái)說(shuō),main 包應(yīng)該很簡(jiǎn)潔。我們?cè)?main 包中會(huì)做一些命令行參數(shù)解析、資源初始化、日志設(shè)施初始化、數(shù)據(jù)庫(kù)連接初始化等工作,之后就會(huì)將程序的執(zhí)行權(quán)限交給更高級(jí)的執(zhí)行控制對(duì)象。另外,也有一些 Go 項(xiàng)目將 cmd 這個(gè)名字改為 app 或其他名字,但它的功能其實(shí)并沒(méi)有變。
接著我們來(lái)看 pkgN 目錄,這是一個(gè)存放項(xiàng)目自身要使用、同樣也是可執(zhí)行文件對(duì)應(yīng) main 包所要依賴的庫(kù)文件,同時(shí)這些目錄下的包還可以被外部項(xiàng)目引用。
然后是 go.mod 和 go.sum,它們是 Go 語(yǔ)言包依賴管理使用的配置文件。我們前面說(shuō)過(guò),Go 1.11 版本引入了 Go Module 構(gòu)建機(jī)制,這里我建議你所有新項(xiàng)目都基于 Go Module 來(lái)進(jìn)行包依賴管理,因?yàn)檫@是目前 Go 官方推薦的標(biāo)準(zhǔn)構(gòu)建模式。
對(duì)于還沒(méi)有使用 Go Module 進(jìn)行包依賴管理的遺留項(xiàng)目,比如之前采用 dep、glide 等作為包依賴管理工具的,建議盡快遷移到 Go Module 模式。Go 命令支持直接將 dep 的 Gopkg.toml/Gopkg.lock 或 glide 的 glide.yaml/glide.lock 轉(zhuǎn)換為 go.mod。
最后我們?cè)賮?lái)看看 vendor 目錄。vendor 是 Go 1.5 版本引入的用于在項(xiàng)目本地緩存特定版本依賴包的機(jī)制,在 Go Modules 機(jī)制引入前,基于 vendor 可以實(shí)現(xiàn)可重現(xiàn)構(gòu)建,保證基于同一源碼構(gòu)建出的可執(zhí)行程序是等價(jià)的。
不過(guò)呢,我們這里將 vendor 目錄視為一個(gè)可選目錄。原因在于,Go Module 本身就支持可再現(xiàn)構(gòu)建,而無(wú)需使用 vendor。 當(dāng)然 Go Module 機(jī)制也保留了 vendor 目錄(通過(guò) go mod vendor 可以生成 vendor 下的依賴包,通過(guò) go build -mod=vendor 可以實(shí)現(xiàn)基于 vendor 的構(gòu)建)。一般我們僅保留項(xiàng)目根目錄下的 vendor 目錄,否則會(huì)造成不必要的依賴選擇的復(fù)雜性。
當(dāng)然了,有些開(kāi)發(fā)者喜歡借助一些第三方的構(gòu)建工具輔助構(gòu)建,比如:make、bazel 等。你可以將這類外部輔助構(gòu)建工具涉及的諸多腳本文件(比如 Makefile)放置在項(xiàng)目的頂層目錄下,就像 Go 創(chuàng)世項(xiàng)目中的 all.bash 那樣。
另外,這里只要說(shuō)明一下的是,Go 1.11 引入的 module 是一組同屬于一個(gè)版本管理單元的包的集合。并且 Go 支持在一個(gè)項(xiàng)目 / 倉(cāng)庫(kù)中存在多個(gè) module,但這種管理方式可能要比一定比例的代碼重復(fù)引入更多的復(fù)雜性。 因此,如果項(xiàng)目結(jié)構(gòu)中存在版本管理的“分歧”,比如:app1 和 app2 的發(fā)布版本并不總是同步的,那么我建議你將項(xiàng)目拆分為多個(gè)項(xiàng)目(倉(cāng)庫(kù)),每個(gè)項(xiàng)目單獨(dú)作為一個(gè) module 進(jìn)行單獨(dú)的版本管理和演進(jìn)。
當(dāng)然如果你非要在一個(gè)代碼倉(cāng)庫(kù)中存放多個(gè) module,那么新版 Go 命令也提供了很好的支持。比如下面代碼倉(cāng)庫(kù) multi-modules 下面有三個(gè) module:mainmodule、module1 和 module2:
$tree multi-modules
multi-modules
├── go.mod // mainmodule
├── module1
│ └── go.mod // module1
└── module2
└── go.mod // module2
我們可以通過(guò) git tag 名字來(lái)區(qū)分不同 module 的版本。其中 vX.Y.Z 形式的 tag 名字用于代碼倉(cāng)庫(kù)下的 mainmodule;而 module1/vX.Y.Z 形式的 tag 名字用于指示 module1 的版本;同理,module2/vX.Y.Z 形式的 tag 名字用于指示 module2 版本。
如果 Go 可執(zhí)行程序項(xiàng)目有一個(gè)且只有一個(gè)可執(zhí)行程序要構(gòu)建,那就比較好辦了,我們可以將上面項(xiàng)目布局進(jìn)行簡(jiǎn)化:
$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
你可以看到,我們刪除了 cmd 目錄,將唯一的可執(zhí)行程序的 main 包就放置在項(xiàng)目根目錄下,而其他布局元素的功用不變。
3.2 Go 庫(kù)項(xiàng)目的典型結(jié)構(gòu)布局
好了到這里,我們已經(jīng)了解了 Go 可執(zhí)行程序項(xiàng)目的典型布局,現(xiàn)在我們?cè)賮?lái)看看 Go 庫(kù)項(xiàng)目的典型結(jié)構(gòu)布局是怎樣的。
Go 庫(kù)項(xiàng)目?jī)H對(duì)外暴露 Go 包,這類項(xiàng)目的典型布局形式是這樣的:
$tree -F lib-layout
lib-layout
├── go.mod
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
└── pkg2/
└── pkg2.go
我們看到,庫(kù)類型項(xiàng)目相比于 Go 可執(zhí)行程序項(xiàng)目的布局要簡(jiǎn)單一些。因?yàn)檫@類項(xiàng)目不需要構(gòu)建可執(zhí)行程序,所以去除了 cmd 目錄。
而且,在這里,vendor 也不再是可選目錄了。對(duì)于庫(kù)類型項(xiàng)目而言,我們并不推薦在項(xiàng)目中放置 vendor 目錄去緩存庫(kù)自身的第三方依賴,庫(kù)項(xiàng)目?jī)H通過(guò) go.mod 文件明確表述出該項(xiàng)目依賴的 module 或包以及版本要求就可以了。
Go 庫(kù)項(xiàng)目的初衷是為了對(duì)外部(開(kāi)源或組織內(nèi)部公開(kāi))暴露 API,對(duì)于僅限項(xiàng)目?jī)?nèi)部使用而不想暴露到外部的包,可以放在項(xiàng)目頂層的 internal 目錄下面。當(dāng)然 internal 也可以有多個(gè)并存在于項(xiàng)目結(jié)構(gòu)中的任一目錄層級(jí)中,關(guān)鍵是項(xiàng)目結(jié)構(gòu)設(shè)計(jì)人員要明確各級(jí) internal 包的應(yīng)用層次和范圍。
對(duì)于有一個(gè)且僅有一個(gè)包的 Go 庫(kù)項(xiàng)目來(lái)說(shuō),我們也可以將上面的布局做進(jìn)一步簡(jiǎn)化,簡(jiǎn)化的布局如下所示:
$tree -L 1 -F single-pkg-lib-layout
single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└── internal/
簡(jiǎn)化后,我們將這唯一包的所有源文件放置在項(xiàng)目的頂層目錄下(比如上面的 feature1.go 和 feature2.go),其他布局元素位置和功用不變。
好了,現(xiàn)在我們已經(jīng)了解完目前 Go 項(xiàng)目的典型結(jié)構(gòu)布局了。不過(guò)呢,除了這些之外,還要注意一下早期 Go 可執(zhí)行程序項(xiàng)目的經(jīng)典布局,這個(gè)又有所不同。
3.3 早期 Go 可執(zhí)行程序項(xiàng)目的典型布局
很多早期接納 Go 語(yǔ)言的開(kāi)發(fā)者所建立的 Go 可執(zhí)行程序項(xiàng)目,深受 Go 創(chuàng)世項(xiàng)目 1.4 版本之前的布局影響,這些項(xiàng)目將所有可暴露到外面的 Go 包聚合在 pkg 目錄下,就像前面 Go 1.3 版本中的布局那樣,它們的典型布局結(jié)構(gòu)是這樣的:
$tree -L 3 -F early-project-layout
early-project-layout
└── exe-layout/
├── cmd/
│ ├── app1/
│ └── app2/
├── go.mod
├── internal/
│ ├── pkga/
│ └── pkgb/
├── pkg/
│ ├── pkg1/
│ └── pkg2/
└── vendor/
我們看到,原本放在項(xiàng)目頂層目錄下的 pkg1 和 pkg2 公共包被統(tǒng)一聚合到 pkg 目錄下了。而且,這種早期 Go 可執(zhí)行程序項(xiàng)目的典型布局在 Go 社區(qū)內(nèi)部也不乏受眾,很多新建的 Go 項(xiàng)目依然采用這樣的項(xiàng)目布局。
所以,當(dāng)你看到這樣的布局也不要奇怪,你應(yīng)該就明確在這樣的布局下 pkg 目錄所起到的“聚類”的作用了。不過(guò),在這里還是建議你在創(chuàng)建新的 Go 項(xiàng)目時(shí),優(yōu)先采用前面的標(biāo)準(zhǔn)項(xiàng)目布局。
四、Go項(xiàng)目典型項(xiàng)目結(jié)構(gòu)分為五部分
放在項(xiàng)目頂層的 Go Module 相關(guān)文件,包括 go.mod 和 go.sum;
cmd 目錄:存放項(xiàng)目要編譯構(gòu)建的可執(zhí)行文件所對(duì)應(yīng)的 main 包的源碼文件;
項(xiàng)目包目錄:每個(gè)項(xiàng)目下的非 main 包都“平鋪”在項(xiàng)目的根目錄下,每個(gè)目錄對(duì)應(yīng)一個(gè) Go 包;
internal 目錄:存放僅項(xiàng)目?jī)?nèi)部引用的 Go 包,這些包無(wú)法被項(xiàng)目之外引用;
vendor 目錄:這是一個(gè)可選目錄,為了兼容 Go 1.5 引入的 vendor 構(gòu)建模式而存在的。這個(gè)目錄下的內(nèi)容均由 Go 命令自動(dòng)維護(hù),不需要開(kāi)發(fā)者手工干預(yù)。
以上就是深入理解Go中的項(xiàng)目代碼布局的詳細(xì)內(nèi)容,更多關(guān)于Go項(xiàng)目代碼布局的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言標(biāo)準(zhǔn)錯(cuò)誤error全面解析
Go語(yǔ)言中的錯(cuò)誤處理是通過(guò)內(nèi)置的error接口來(lái)實(shí)現(xiàn)的,其中errorString和wrapError是兩種常見(jiàn)的錯(cuò)誤類型實(shí)現(xiàn)方式,errorString通過(guò)errors.New()方法實(shí)現(xiàn),而wrapError則通過(guò)fmt.Errorf()方法實(shí)現(xiàn),支持錯(cuò)誤的嵌套和解析2024-10-10