Go 項(xiàng)目布局的實(shí)戰(zhàn)經(jīng)驗(yàn):別再濫用 pkg 和 util 了
最近微信群里有小伙伴再次提出了靈魂疑問:
Go 項(xiàng)目應(yīng)該怎么組織目錄結(jié)構(gòu)?
這是個(gè)反復(fù)出現(xiàn)的問題。網(wǎng)上確實(shí)有一些過度復(fù)雜的博客和示例倉庫,把很多同學(xué)都整糊涂了。
今天我們來梳理一下常見的“坑”,以及更實(shí)用的項(xiàng)目組織方式。
畢竟 Go 這一門編程語言的核心哲學(xué)是:less is more,少點(diǎn)花里胡哨的層次。
核心原則(一句話版)
- 先把可用的東西做出來,再考慮拆包。
- 不要為了 “某種慣例” 或 “看起來專業(yè)” 而過度設(shè)計(jì)。
- internal/、pkg/、cmd/ 這些都是工具,并不是每個(gè)項(xiàng)目都必須要用的。
要點(diǎn)與示例
1. main 放哪里?
建議:主程序(CLI / server)放在倉庫根目錄的 main(或直接 main.go)是最簡單也最常用的。
優(yōu)勢之一是安裝/運(yùn)行最短路徑。
便于 go install:
# 最短最干凈的安裝方式(root 有 main)
go install github.com/eddycjy/project@latest只有庫(library)的倉庫自然沒有 main。如果既有庫又有二進(jìn)制,可以把 main 放子目錄(很多人習(xí)慣 cmd/yourcmd/,但 cmd 本身并非必須)。
示例(根目錄有 main):
/project
go.mod
main.go // 程序入口
lib/ // 如果你還有對外庫
internal/ // 可選
README.md或者把可復(fù)用代碼放到一個(gè)清晰命名的包,而不是把 main 挪得過遠(yuǎn)。
2. internal/ 是特性不是儀式
internal/ 的機(jī)制是 Go 工具鏈強(qiáng)制的:internal 下的代碼只能被父目錄(和子級樹)里的包導(dǎo)入。
好處是可以阻止外部依賴,但不是每個(gè)項(xiàng)目都需要它。
這時(shí)候就涉及到什么時(shí)候用?
當(dāng)你真的有很多對外不暴露但跨包復(fù)用的代碼,并且項(xiàng)目會(huì)被大量第三方使用時(shí)才考慮 internal/。
對絕大多數(shù)小中型項(xiàng)目來說,不用 internal/ 更簡單、更靈活。
示例(使用場景):
/project
internal/
secrets/ // 只有 project 內(nèi)部可以 import
pkg/ // 對外可用庫(慎用 pkg_/)
main.go3. 別盲目使用 pkg/
pkg/ 是歷史遺留的慣例,在現(xiàn)代 Go 里沒必要把所有對外包都塞進(jìn) pkg/。
包名與路徑應(yīng)以可讀性與語義為核心。把包放到頂層(/auth、/db、/storage)通常更直觀,且導(dǎo)入路徑更短。
示例對比:
不推薦(多一層 pkg):
import "github.com/you/project/pkg/storage"更推薦(語義清晰):
import "github.com/you/project/storage"4. 不要亂建 util、common 等
“工具類” 包看起來方便,但會(huì)變成隨手塞東西的垃圾倉庫。把函數(shù)/類型放到語義化更強(qiáng)的包里,或放在最常用的使用位置鄰近代碼,而不是一個(gè)籠統(tǒng)的 util。
反面示例:
// util/strings.go
package util
func Reverse(s string) string { ... }更好寫法(語義化):
// text/reverse.go
package text
func Reverse(s string) string { ... }
// 或者直接放在使用它的包里,例如 handler/text_helpers.go5. 包不要太多(也別千行一包)
Go 可以在一個(gè)包里有多個(gè)文件,這一點(diǎn)要善用。每新增一個(gè)包,就可能增加依賴、回環(huán)風(fēng)險(xiǎn)和遷移成本。相反,也不要把完全不相關(guān)的代碼塞成一個(gè)冗長的包——保持“以用途/語義分包”。
經(jīng)驗(yàn)規(guī)則:
- 如果一組代碼有同一語義與同一生命周期,放到同一個(gè)包。
- 每個(gè)包最好能在 200–1000 行范圍內(nèi)(這不是硬性規(guī)則,只是可讀性提醒)。
- 切包優(yōu)先按“用途”而不是“文件大小”。
6. 文件別太細(xì)碎
許多人喜歡把每個(gè)小函數(shù)放不同文件,結(jié)果翻代碼像翻書頁。合理把相關(guān)函數(shù)聚合到同一文件,便于閱讀。
避免把每個(gè) tiny helper 分成獨(dú)立文件。
7. 語義化命名優(yōu)先于目錄深度
庫名、包名與目錄名應(yīng)體現(xiàn)用途。例如 applog 比 util/log 更有意義。
這樣看代碼的同學(xué)通過 import 一眼能看出大致的用途。
8. 版本管理和 semver 建議
建議盡量使用 0.x 階段語義化版本(保守上 v0.x),在你要打破 API 時(shí)給出明確變更說明,而不是過早把版本固定為 v2/v3,導(dǎo)致用戶為小改動(dòng)分叉?zhèn)}庫。
換句話說:先發(fā)布、后演進(jìn),記錄變更而不是封閉。
推薦的最小倉庫模板(實(shí)戰(zhàn))
下面給出一個(gè)適合多數(shù)小中型項(xiàng)目的極簡布局,能覆蓋 CLI / library 混合場景:
/project
go.mod
main.go // 如果是二進(jìn)制,把入口放這里
README.md
config/ // 配置相關(guān)包
storage/ // 存儲(chǔ)邏輯
api/ // HTTP handler / grpc / rpc
tools/ // 非構(gòu)建、非導(dǎo)出的腳本 & 工具(可忽略 go build)
docs/如果你確實(shí)需要多個(gè)可發(fā)布包,再考慮增加清晰命名的子包,而不是 pkg/ 通用層。
Go 官方建議,要關(guān)注細(xì)節(jié)
Go 官方確實(shí)給了一份指南:go.dev/doc/modules/layout[1]。
里面有句話經(jīng)常被曲解:
Larger packages or commands may benefit from splitting… Initially, put them in internal/.
這里的重點(diǎn)其實(shí)是 larger 和 may。
結(jié)果很多人一上來就機(jī)械套用:不管項(xiàng)目大小,先建個(gè) internal/;
現(xiàn)實(shí)是要知道目錄不是 “一步到位” 的事,需要階段性調(diào)整和設(shè)計(jì)。
總結(jié)
我們要回歸 Go 的哲學(xué):簡單優(yōu)先,先能跑,再優(yōu)雅。多數(shù)團(tuán)隊(duì)在項(xiàng)目初期做的過度工程(把 internal/、pkg/、cmd/ 都直接套上)更多是為了 “看起來成熟”,但長期結(jié)果往往是維護(hù)負(fù)擔(dān)增加。
把注意力放在清晰的包命名、合理的功能邊界、良好的 README 上,必要時(shí)再重構(gòu)目錄結(jié)構(gòu)和演進(jìn)會(huì)比較好。
參考資料
[1] go.dev/doc/modules/layout: https://go.dev/doc/modules/layout

























