← 返回文章列表

解读 golang-standards/project-layout

Go
  • # 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 和普通目录最大的差别。pkgcmdconfigs 都主要靠约定表达语义,internal 则有工具链保证。更准确地说,internal 表达的是访问边界:这部分代码属于当前项目,不承诺给外部项目使用。

project-layout 里的 internal/app 和 internal/pkg

project-layout 给出的结构大概是这样:

internal/
├── app/
│   └── _your_app_/
└── pkg/
    └── _your_private_lib_/

这两个目录名容易造成误解。它们并不是 Go 规定的特殊目录,只是项目作者建议的一种分组方式。

internal/app 通常放应用自己的业务代码。比如一个项目里有 apiworkerscheduler 三个可执行程序,可以把它们对应的业务逻辑放在:

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/appinternal/pkg 只是一个起点,不是必须遵守的分层。小项目里,直接这样组织可能更清楚:

internal/
├── user/
├── order/
├── billing/
└── notification/

这种结构按业务领域拆分,比先分 apppkg 再继续往下找更直接。

问题出现在项目变大之后。如果所有业务都塞进 internal/app,所有共享代码都塞进 internal/pkg,这两个目录很快会变成新的大杂烩。到那时,internal 只是把混乱往下一层推了,并没有真的建立边界。

更好的判断方式是先问:

  • 这段代码是否允许外部项目导入?
  • 如果不允许,它属于哪个业务边界?
  • 它是某个应用独有,还是多个内部应用共享?
  • 它是否真的需要成为“共享包”?

只有回答完这些问题,internal 目录才有意义。否则它只是一个看起来专业的文件夹。

一个更实用的 internal 心智模型

我会把 internal 理解成“项目私有 API 的集合”。它里面可以有应用代码,也可以有共享库,但它们共同点是:这些代码不对外承诺稳定性。

如果某个包还没想清楚是否要对外暴露,先放进 internal 往往更安全。等它真的稳定、抽象足够清楚,并且外部项目确实需要复用,再考虑移动到顶层 pkg。反过来,把还没成型的代码一开始就放到 pkg,会过早制造公开 API 的压力。

所以,project-layout 里最值得学习的不是 internal/appinternal/pkg 这两个名字,而是它提醒我们:Go 项目结构首先要表达边界。目录不是为了好看,而是为了让别人一眼看出哪些代码能用,哪些代码不能随便用。