首页
/ Standard Go Project Layout:基于 project-layout 仓库的 Go 项目目录结构规范详解

Standard Go Project Layout:基于 project-layout 仓库的 Go 项目目录结构规范详解

2026-09-05 23:38:03作者:裘旻烁

本文以 Go 社区事实标准仓库 project-layout 的说明文档为主线,逐目录讲解 /cmd/internal/pkg/vendor 等核心目录与 /api/configs/scripts/deployments 等辅助目录的职责、适用场景与取舍原则,并结合仓库中真实的目录骨架(占位目录、go.modMakefile)说明如何把这套布局落地到自己的 Go 项目中。

一、总览:这是一套"基本骨架",而不是官方标准

project-layout 提供的是一套基本(basic)的 Go 应用项目目录规划,其"基本"体现在两个层面:

  1. 只关注整体 layout,不关注目录里装什么——它刻意保持高层抽象,不会深入到诸如 Clean Architecture 之类更细粒度的项目内部结构;
  2. 不施加任何特定的 Go 包结构——它是有意保持通用(intentionally generic)的,目的是给出一组被广泛接受的"目录语言",让团队成员对新项目的组织方式有共同预期。

文档同时明确强调:这套布局不是 Go 核心团队官方定义的标准("NOT an official standard defined by the core Go dev team"),而是 Go 生态中常见的历史与新兴项目布局模式的集合。其中部分模式(如 cmdinternal)比其他模式更受欢迎,并附带了一些面向足够大的真实应用的支撑目录。Go 核心团队在官方文档的 "Organizing a Go module" 页面中也提供了关于 Go 项目组织方式的通用指南(包含下文将讲的 internalcmd 目录模式),两套资料可以互相印证。

什么时候该用、什么时候不该用

文档给出了非常明确的适用边界:

  • 正在学 Go、做 PoC 或给自己写小工具时,这套布局是"过度设计"(overkill)——一个 main.go 加一个 go.mod 就足够了;
  • 随着项目增长,如果不注意代码组织,最终会得到一堆"隐藏依赖(hidden dependencies)+ 大量全局状态(global state)"的混乱代码;
  • 协作人数增加后需要更强的结构,这时应引入管理包/库的共同方式;
  • 当项目开源、或知道有其他项目会 import 你的代码时internal 私有包和私有代码才真正重要。

文档推荐的使用姿势是一句话:"克隆本仓库,保留你需要的部分,删掉其余的!"(Clone the repository, keep what you need and delete everything else!)——目录存在不代表必须全部使用,没有任何一种模式适用于每个项目,甚至连 vendor 模式都不是万能的。

Go Modules:布局成立的前提

自 Go 1.14 起,Go Modules 已"production ready"。文档的建议是:除非有特定理由不用,否则一律使用 Go Modules;使用它之后,就不必再关心 $GOPATH 以及项目应该放在哪里。

仓库自带的 go.mod(见 go.mod)印证了这一点,内容非常精简:

module github.com/YOUR-USER-OR-ORG-NAME/YOUR-REPO-NAME

go 1.19

关于 module path 的几个注意点(以当前仓库实际内容为准):

  • 这份 go.mod 假设项目托管在 GitHub,但这不是强制要求,module path 可以任意;
  • module path 的第一段(first component)应包含一个点——当前版本的 Go 已不再强制检查,但如果你使用稍旧的 Go 版本,构建失败时不必惊讶;
  • 文档提示可查阅 golang/go 的 Issue #37554 与 #32819 了解该规则的来龙去脉。

命名、格式化与风格检查

文档建议:遇到命名、格式化与风格问题,先运行 gofmtstaticcheck。原来的标准 linter golint 已废弃且不再维护,文档推荐使用仍在维护的 linter(如 staticcheck)。此外还列出了一批 Go 代码风格参考资料("Names" 演讲幻灯片、Effective Go 的命名章节、Package Names 博客、Code Review Comments wiki、rakyll 的 Go 包风格指南)以及 GopherCon 系列演讲(Peter Bourgon《Best Practices for Industrial Programming》、Edward Muller《Go Anti-Patterns》、Kat Zien《How Do You Structure Your Go Apps》等),这里只保留主题线索,读者可按名称自行检索原文。

二、仓库结构一览:占位目录即"可复制的模板"

理解这套布局最直观的方式,是看 project-layout 仓库本身的目录树。当前仓库(即 README_fa.md 所在的仓库)正是按其描述自洽组织的"活模板":

.
├── api/                        # OpenAPI/Swagger 规格、JSON Schema、协议定义
├── assets/                     # 图片、logo 等仓库级静态资源
├── cmd/
│   └── _your_app_/             # 占位目录:每个可执行程序一个子目录
├── configs/                    # 配置文件模板 / 默认配置
├── deployments/                # IaaS/PaaS/容器编排部署配置
├── docs/                       # 设计与用户文档
├── examples/                   # 应用与公开库的示例
├── githooks/                   # Git hooks
├── init/                       # systemd/upstart/sysv、runit/supervisord 配置
├── internal/
│   ├── app/
│   │   └── _your_app_/         # 私有应用代码
│   └── pkg/
│       └── _your_private_lib_/ # 私有共享库代码
├── pkg/
│   └── _your_public_lib_/      # 对外公开的库代码
├── scripts/                    # build/install/analysis 脚本
├── test/                       # 外部测试程序与测试数据
├── third_party/                # fork 的代码与第三方工具
├── tools/                      # 项目辅助工具
├── web/
│   ├── app/
│   ├── static/
│   └── template/               # Web 应用:SPA、静态资源、服务端模板
├── website/                    # 项目站点数据
├── LICENSE.md
├── Makefile                    # 刻意保持极简
├── go.mod                      # module 占位声明
└── README.md(及 17 种语言的译本,含 README_fa.md)

从源码结构看,几个占位目录(如 cmd/your_apppkg/your_public_libinternal/app/your_appinternal/pkg/your_private_libweb/static)内部只放了一个 .keep 空文件——它们的作用就是在克隆后告诉你"这里应该放什么",保留自己需要的部分、删除其余,正是文档建议的落地方式。

两个细节值得注意:

  • Makefile 的全部内容只有一行注释 # note: call scripts from /scripts。这与下文 /scripts 章节的设计意图直接对应:根级 Makefile 保持小而简单,具体操作脚本下沉到 /scripts 目录
  • 仓库没有 vendor/ 目录,与"vendor 不是万能的、使用 module proxy 时甚至可以完全不需要它"的说法一致。

三、Go 核心目录

3.1 /cmd:主应用入口,可执行文件名 = 子目录名

职责:存放本项目的所有主应用。规则是:每个应用对应一个子目录,目录名必须与你想要的可执行文件名一致(例如 /cmd/myapp 产出可执行文件 myapp)。

文档的核心告诫:

  • 不要在应用目录里堆大量代码。如果你认为某段代码可以被其他项目 import 使用,它就应放进 /pkg;如果代码不可复用、或你不想让别人复用,就放进 /internal。"你会惊讶于别人会拿你的代码做什么,所以请把你的意图表达得足够明确!"
  • 常见的做法是:main 函数非常小,只做一件事——import 并调用 /internal/pkg 中的代码,不再做别的。

仓库中的 cmd/README.md 给出了同一规则的完整版,并列举了 velero、moby、prometheus、influxdb、kubernetes、dapr、go-ethereum 等主流项目均按此模式组织 cmd/,可作旁证。

3.2 /internal:由 Go 编译器强制执行的"私有包"

职责:私有应用与私有库代码,即你不希望其他项目 import 的代码

这条规则的权威性来自 Go 语言本身:从源码结构看,internal 的可见性约束由 Go 编译器直接强制执行(Go 1.4 release notes 首次引入 internal packages 机制)——把包放进 internal 目录后,其他包只有在与它"拥有共同祖先(common ancestor)"时才能 import 它。internal 也是 Go 官方文档唯一点名、并享有特殊编译器处理的目录。

使用要点(与 internal/README.md 一致):

  • 不限于顶层一个 internal:可以在项目树的任意层级、放置多个 internal 目录;

  • 可选的二级结构:为区分"共享的私有代码"与"非共享的私有应用代码",可以按仓库的示例组织为

    • /internal/app/<应用名> —— 具体应用代码(本仓库占位为 internal/app/_your_app_);
    • /internal/pkg/<私有库名> —— 被这些应用共享的私有代码(本仓库占位为 internal/pkg/_your_private_lib_)。

    文档强调这种细分不是强制的(小项目尤其不必),但它提供了"这个包预期如何使用"的视觉线索。

3.3 /pkg:对外承诺"安全可用"的库代码

职责:允许外部应用使用的库代码(例如 /pkg/mypubliclib)。

文档的态度很克制,有三层含义:

  1. 其他项目会 import 这里的库并预期它们能正常工作,所以放入之前"想三遍"——这里代表了一种事实上的对外 API 承诺;
  2. internal 才是更强保障:确保私有包不被 import 的更好手段是 internal(因为由 Go 语言机制强制执行);/pkg 的价值在于显式地(explicitly)告诉外部使用者"这里的代码供你安全使用";
  3. /pkg 还有工程层面的理由:当根目录混入大量非 Go 组件和目录时,把 Go 代码聚拢到一个地方,方便运行各类 Go 工具(GopherCon 2018 的 Best Practices for Industrial Programming、Kat Zien 的演讲、GoLab 2018 的 Project layout patterns in Go 都提到过这一点)。

仓库中的 pkg/README.md 进一步说明:pkg 目录不是被普遍接受的模式——"对每个用它的主流仓库,你都能找到十个不用的",用不用取决于你自己;但即便如此,懂得这个约定的人仍然占多数。该文件还列出了 containerd、jaeger、istio、helm、k3s、kubernetes、grafana、influxdb、cockroach 等大量采用该模式的知名仓库清单。

起源考据:早期 Go 源码自身就用 pkg 存放包,社区项目随后效仿这一模式(Brad Fitzpatrick 曾在推文中解释过这一背景)。

适用建议:如果项目真的很小、多一层嵌套没有收益,不用 pkg 完全没问题;等根目录变得拥挤(尤其有大量非 Go 组件)时再考虑引入。

3.4 /vendor:依赖的落地方式

职责:存放应用依赖(手动管理,或由依赖管理工具管理,如今内置的 Go Modules 就是标准答案)。

关键操作与版本边界:

  • go mod vendor 命令会为项目创建 /vendor 目录;
  • 如果使用的 Go 版本不是 1.14 及以上(1.14 起默认启用 vendor 自动检测),可能需要给 go build 显式加上 -mod=vendor 标志;
  • 如果你在构建一个库(library),不要 commit 你的应用依赖
  • 自 Go 1.13 起,module proxy 特性启用(默认使用官方 proxy.golang.org 作为模块代理服务器)。文档建议读者自行评估代理方案是否满足自身要求与限制——如果满足,你甚至完全不需要 vendor 目录

四、服务类应用目录:/api

职责:存放服务接口的定义性文件——OpenAPI/Swagger 规格、JSON Schema 文件、协议定义文件。

api/README.md 中列举了 kubernetes、moby 等项目把 API 定义集中放在 api/ 的做法。对提供 API 的服务而言,这一目录让"接口契约"与实现代码物理分离,便于文档生成与跨语言消费。

五、Web 应用目录:/web

职责:Web 应用特有组件——静态 Web 资源(static assets)、服务端模板(server-side templates)与单页应用(SPA)。

当前仓库的 web/ 目录恰好给出了三分法的占位实现:web/app(SPA 构建产物或入口)、web/static(CSS/JS/图片等静态资源)、web/template(服务端渲染模板)。三个子目录内同样只含 .keep 占位文件,克隆后可按需要保留或删除。

六、通用应用目录

6.1 /configs:配置文件模板与默认值

存放配置文件的模板或默认配置。文档特别提示:confdconsul-template 的模板文件应放在这里——也就是说 /configs 同时承担"仓库内默认配置"与"渲染型配置模板"两种角色。见 configs/README.md

6.2 /init:系统初始化与进程守护配置

存放系统 init(systemd、upstart、sysv)以及进程管理器/守护器(runit、supervisord)的配置。适合有"系统服务"交付形态的项目。

6.3 /scripts:操作脚本,让根 Makefile 保持简单

职责:执行各种 build、install、analysis 等操作的脚本。

设计意图:这些脚本让根级 Makefile 保持小且简单。仓库自身的 Makefile 就是这一理念的最纯粹示范——整份文件只有一行 # note: call scripts from /scripts,所有实际操作都被委托给 scripts/ 目录。scripts/README.md 列举了 kubernetes/helm、cockroachdb/cockroach、hashicorp/terraform 等项目的同类做法(文档中还以 Terraform 的根 Makefile 作为"保持精简"的参照)。

6.4 /build:打包(Packaging)与持续集成(CI)

文档将 /build 拆为两块职责(原文为两个同名单元,含义互补):

  • 打包:把云镜像(AMI)、容器(Docker)、操作系统包(deb、rpm、pkg)的打包配置与脚本放在 /build/package
  • 持续集成:把 CI(travis、circle、drone 等)的配置与脚本放在 /build/ci。文档特别提醒:部分 CI 工具(如 Travis CI)对配置文件的存放位置非常挑剔,优先把配置统一放在 /build/ci,再在工具期望的位置建立软链接指回它(在允许的情况下)。

6.5 /deployments:部署与编排

存放 IaaS、PaaS、系统与容器编排的部署配置和模板,典型如 docker-compose、kubernetes/helm、terraform。文档注意:有些仓库(尤其是以 kubernetes 部署的应用)习惯把该目录命名为 deploy/——理解他人代码时要意识到这种命名变体。

6.6 /test:外部测试程序与测试数据

职责:额外的外部测试应用与测试数据(区别于包内单元测试)。使用规则:

  • /test 的内部结构可自由组织;
  • 大项目建议设一个数据子目录,例如 test/datatest/testdata——testdata 是 Go 工具链约定忽略的目录名;
  • 补充:Go 同样会忽略以 ._ 开头的目录/文件,因此命名测试数据目录的余地比想象中大。

test/README.md 以 openshift/origin 的 test/(测试数据位于 testdata 子目录)为例。

七、其他辅助目录

目录 职责 仓库内文档
/docs 设计与用户文档(与 godoc 生成的 API 文档互补) docs/README.md
/tools 本项目的辅助工具;注意这些工具可以 import /pkg/internal 中的代码 tools/README.md
/examples 为你的应用和/或公开库提供的示例 examples/README.md
/third_party 外部辅助工具、fork 的代码与其他第三方工具(如 Swagger UI)
/githooks Git hooks
/assets 随仓库走的资源(图片、logo 等)
/website 若不使用 GitHub Pages,这里放项目站点数据 website/README.md

各目录 README 中还给出了主流项目的同类实践(/docs:hugo、openshift/origin、dapr;/tools:istio、dapr;/examples:nats.go、docker-slim、packer;/website:vault、perkeep),可作对照参考。

八、不应存在的目录:/src

文档用一整节明确反对在 Go 项目根下建 src/

  • 出现 src/ 的 Go 项目通常来自 Java 开发者(src 在 Java 生态中是常见模式),"你并不想让 Go 代码和 Go 项目看起来像 Java";
  • 不要混淆两个 src:项目级的 /src 与 Go 为 workspace 使用的 /src$GOPATH 环境变量指向当前 workspace(非 Windows 系统默认 $HOME/go),workspace 顶层包含 /pkg/bin/src 三个目录,真正的项目会落在 /src 之下。因此若项目自带 /src,实际路径会变成类似 /some/path/to/workspace/src/your_project/src/your_code.go 的双重嵌套;
  • Go 1.11 之后项目可以放在 GOPATH 之外,但这不意味着采用 src/ 布局是好主意。

九、质量信号:Badges

文档列出的徽章(徽章本身不在本文复现,仅说明用途与获取方式):

  1. Go Report Card:扫描 gofmtgo vetgocyclogolintineffassign、license 与拼写(misspell),把服务上的仓库引用替换为项目自己的引用即可使用;
  2. GoDoc(已废弃):文档中已用删除线标注该徽章失效,作为历史说明保留;
  3. Pkg.go.dev:Go 生态新的"发现与文档"目的地,可用其 badge 生成工具创建徽章;
  4. Release:展示项目最新 release 版本号,把链接指向自己的仓库即可。

十、落地建议与总结

综合文档与仓库结构,把这套布局落到一个真实项目的步骤可以是:

  1. 克隆 project-layout 仓库(如需 git clone,可指向你的托管副本;本仓库的主 README 见 README.md,本文对应的波斯语译本见 README_fa.md);
  2. 保留所需目录、删除其余——尤其是 go.modmodule github.com/YOUR-USER-OR-ORG-NAME/YOUR-REPO-NAME 这一占位 module 声明必须替换为自己的真实路径,go 1.19 版本行按团队工具链调整;
  3. gofmt + staticcheck 兜底命名与格式;
  4. 遵循 /cmd(薄 main)、/internal(编译器强制私有)、/pkg(显式公开承诺)的意图表达,让目录名本身承担文档作用;
  5. 按需启用 /api/web/configs/deployments 等服务型/交付型目录,并保持根 Makefile 极简、脚本下沉 /scripts
  6. 不引入 src/;小项目宁缺勿滥,vendor 仅在代理方案不满足约束时才需要。

文档末尾的 "Notes" 也交代了演进方向:一套"更有主见的(more opinionated)"、附带示例/可复用配置、脚本与代码的项目模板仍是进行中的工作(WIP)——即当前版本刻意只提供骨架,避免替项目做架构决策。这正与全文基调一致:目录结构是团队协作的公共语言,而不是架构本身

登录后查看全文
热门项目推荐
相关项目推荐