1. main 放哪里?
除了library库以外,main.go放在项目根目录下; 既有库又有二进制,可以把main放进cmd目录,非必须;
建议:主程序(CLI / server)放在仓库根目录的 main(或直接 main.go)是最简单也最常用的。
优势之一是安装/运行最短路径。
便于 go install:
最短最干净的安装方式(root 有 main)
go install github.com/eddycjy/project@latest
示例(根目录有 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.go
3. 别盲目使用 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.go
5. 包不要太多(也别千行一包)
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 上,必要时再重构目录结构和演进会比较好。