解读 golang-standards/project-layout
- # go
- # project-layout
- # architecture
这篇文章还在编辑中,内容可能会继续补充、调整或重写。
golang-standards/project-layout 不是 Go 官方标准,而是一份社区整理的项目目录结构参考。解读它时,重点不是照抄目录,而是理解每个目录背后的边界。
展开查看项目目录结构
project-layout/
├── api/
├── assets/
├── build/
│ ├── ci/
│ └── package/
├── cmd/
│ └── _your_app_/
├── configs/
├── deployments/
├── docs/
├── examples/
├── githooks/
├── init/
├── internal/
│ ├── app/
│ │ └── _your_app_/
│ ├── pkg/
│ │ └── _your_private_lib_/
├── pkg/
│ └── _your_public_lib_/
├── scripts/
├── test/
├── third_party/
├── tools/
├── vendor/
├── web/
├── website/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENSE.md
├── Makefile
├── README.md
├── README_*.md
└── go.mod为什么只看 internal
project-layout 里大部分目录的用途,仓库 README 已经写得很清楚:cmd 放可执行入口,configs 放配置模板,deployments 放部署定义,docs 放项目文档,scripts 放脚本,test 放额外测试资产。继续逐个解释这些目录,意义不大。
真正值得单独研究的是 /internal。因为它不是普通的社区约定,而是 Go 工具链认识的目录。也就是说,internal 不是“我觉得这里是内部代码”,而是 go 命令真的会根据 import 路径限制谁能引用它。
internal 的真正含义
Go 对 internal 的规则很简单:如果一个包的 import path 中包含 internal,那么只有 internal 父目录所在目录树里的代码可以导入它。
例如:
example.com/project/
├── cmd/
│ └── api/
│ └── main.go
└── internal/
└── user/
└── service.go
cmd/api/main.go 可以导入:
import "example.com/project/internal/user"
但另一个项目不能导入:
import "example.com/project/internal/user"
这就是 internal 和普通目录最大的差别。pkg、cmd、configs 都主要靠约定表达语义,internal 则有工具链保证。更准确地说,internal 表达的是访问边界:这部分代码属于当前项目,不承诺给外部项目使用。
project-layout 里的 internal/app 和 internal/pkg
project-layout 给出的结构大概是这样:
internal/
├── app/
│ └── _your_app_/
└── pkg/
└── _your_private_lib_/
这两个目录名容易造成误解。它们并不是 Go 规定的特殊目录,只是项目作者建议的一种分组方式。
internal/app 通常放应用自己的业务代码。比如一个项目里有 api、worker、scheduler 三个可执行程序,可以把它们对应的业务逻辑放在:
internal/
└── app/
├── api/
├── worker/
└── scheduler/
这样做的好处是:cmd/api 只保留启动入口,真正的业务组装、路由注册、服务初始化都放到 internal/app/api 里。cmd 不会变成一个越来越大的业务目录。
internal/pkg 则通常放当前项目内部共享的库。比如多个应用都需要日志、配置、鉴权、任务队列封装,可以放在:
internal/
└── pkg/
├── auth/
├── config/
├── logger/
└── queue/
它和顶层 pkg 的区别在于:顶层 pkg 暗示“外部项目可以导入”,而 internal/pkg 暗示“只有本项目内部共享”。这不是语气差异,而是访问边界差异。
不要机械套 internal/app 和 internal/pkg
internal/app 和 internal/pkg 只是一个起点,不是必须遵守的分层。小项目里,直接这样组织可能更清楚:
internal/
├── user/
├── order/
├── billing/
└── notification/
这种结构按业务领域拆分,比先分 app、pkg 再继续往下找更直接。
问题出现在项目变大之后。如果所有业务都塞进 internal/app,所有共享代码都塞进 internal/pkg,这两个目录很快会变成新的大杂烩。到那时,internal 只是把混乱往下一层推了,并没有真的建立边界。
更好的判断方式是先问:
- 这段代码是否允许外部项目导入?
- 如果不允许,它属于哪个业务边界?
- 它是某个应用独有,还是多个内部应用共享?
- 它是否真的需要成为“共享包”?
只有回答完这些问题,internal 目录才有意义。否则它只是一个看起来专业的文件夹。
一个更实用的 internal 心智模型
我会把 internal 理解成“项目私有 API 的集合”。它里面可以有应用代码,也可以有共享库,但它们共同点是:这些代码不对外承诺稳定性。
如果某个包还没想清楚是否要对外暴露,先放进 internal 往往更安全。等它真的稳定、抽象足够清楚,并且外部项目确实需要复用,再考虑移动到顶层 pkg。反过来,把还没成型的代码一开始就放到 pkg,会过早制造公开 API 的压力。
所以,project-layout 里最值得学习的不是 internal/app 或 internal/pkg 这两个名字,而是它提醒我们:Go 项目结构首先要表达边界。目录不是为了好看,而是为了让别人一眼看出哪些代码能用,哪些代码不能随便用。