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


























