从一个文件开始
一个 Go 程序完全可以只写在一个 main.go 里,小工具也应该如此。下面这个完整的程序在一个包里解析输入、完成工作并打印报告:
当它变大时,把它拆成多个文件,放在同一个目录、同一个包中(parse.go、report.go、main.go)。同一个包中的文件共享所有标识符,所以不需要导出或导入任何东西。对于几千行代码的项目,这往往就是它需要的全部结构。
记住,由多个文件组成的 package main 必须作为一个包来运行:用 go run .,而不是 go run main.go。否则文件会被单独编译,其他文件中定义的名字就会报 undefined 错误。
没有强制的布局
除了“一个目录就是一个包”之外,Go 不要求任何目录结构。官方指南是 go.dev 上的 Organizing a Go module 页面,它介绍的是几种形态,而不是规则。
GitHub 仓库 golang-standards/project-layout 被广泛照搬,也常被误认为是一种标准。它是社区从大型项目中收集来的惯例,时任 Go 技术负责人的 Russ Cox 曾在那里开了一个 issue,说明它并不是 Go 的标准。它的大多数目录(pkg/、api/、build/、deployments/)只对大型代码库有意义。把整套骨架复制到新项目里,只会带来空文件夹和很深的导入路径,毫无益处。
go 工具强制执行的规则很少:
- 一个目录就是一个包。目录中所有
.go文件(使用_test后缀包名的_test.go文件除外)共用一个包名。 - 导入路径是模块路径加上目录路径。
- 名为
internal的目录会限制谁能导入其中的内容。 - 名为
testdata的目录,以及以.或_开头的目录,会被 go 工具忽略。
常见的形态
单个命令
todo/
├── go.mod module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go
扁平结构,所有代码都在 package main 中。go install github.com/you/todo@latest 可以直接使用,生成的可执行文件名为 todo。
一个库
slug/
├── go.mod module github.com/you/slug
├── slug.go package slug
├── slug_test.go
├── example_test.go
└── internal/
└── table/ helpers the public package uses, hidden from users
包位于模块根目录,所以用户导入 github.com/you/slug 并调用 slug.Make(...)。任何你不想作为公开 API 来维护的东西,都放到 internal/ 下。
一个服务或多个可执行文件
shop/
├── go.mod module github.com/you/shop
├── cmd/
│ ├── shop-api/
│ │ └── main.go package main: flags, config, wiring
│ └── shop-worker/
│ └── main.go
├── internal/
│ ├── order/ package order: domain types and logic
│ │ ├── order.go
│ │ └── order_test.go
│ ├── store/ package store: database access
│ └── httpapi/ package httpapi: handlers, routing
├── migrations/
└── README.md
cmd/<name>/main.go 为每个可执行文件准备一个目录,目录名就是可执行文件的名字(go build ./cmd/shop-api 生成 shop-api)。每个 main 都保持精简:读取配置、构造依赖、启动服务器。真正的代码放在 internal/ 下的包里,两个可执行文件都能使用,而其他模块都不能。
大多数 Go 服务最终都会收敛到这种布局。当你有了第二个可执行文件,或者确实有理由拆分包时再采用它,不必从第一天就这样做。
internal/ 由 go 命令强制执行
internal/ 下的包只能被以 internal 的父目录为根的目录树中的代码导入。从另一个模块导入,构建就会失败:
package example.com/b
main.go:6:2: use of internal package example.com/a/internal/secret not allowed
这让 internal/ 成为控制 API 范围的工具。其中的代码可以随意修改,因为你知道所有调用方都在你自己的仓库里。对于应用程序,把几乎所有东西都放进 internal/ 是合理的。对于库,它把你承诺保持稳定的部分和不做承诺的部分分开。
internal 在任何层级都有效:shop/internal/order 对整个 shop/ 可见,而 shop/internal/order/internal/pricing 只在 shop/internal/order/ 内部可见。
包的命名
包名会出现在每一处调用中,所以它比目录树更重要。
- 简短、全小写、一个单词:
order、store、httpapi。不用下划线,也不用大小写混写。 - 按它提供的功能命名,而不是按它包含的东西。
util、common、helpers、misc和models什么也说明不了,最终会变成杂物堆;Go 团队的风格指南明确不鼓励这样命名。把辅助函数放到使用它的包里,或者放到一个按用途命名的包中(slug、retry)。 - 避免重复。 调用方写的是
order.Order和order.New,所以不要起order.OrderService或order.NewOrder这样的名字。标准库一贯如此:是http.Server,而不是http.HTTPServer。 - 目录名和包名应该一致,命令目录中的
package main除外。两者不一致时,读代码的人必须打开文件才能知道这个导入引入的是什么标识符。
按职责拆分包,而不是按种类拆分。models/、controllers/、services/ 这种拆分方式(在其他生态中很常见)会让每个功能都要改动三个包,而且容易产生循环导入,而 Go 禁止循环导入。围绕领域概念组织的包(order、payment、user),各自把自己的类型和逻辑放在一起。
测试放在哪里
测试和代码放在同一个目录中,文件名为 *_test.go。没有 tests/ 目录树。
order_test.go中的package order:内部测试,可以使用未导出的标识符。- 同一目录中的
package order_test:外部测试,像调用方一样只能看到导出的 API。适合编写示例,也能避免测试中出现循环导入。 - 测试用的数据文件放在测试旁边的
testdata/目录中。go 工具会忽略它,而且测试运行时以包目录作为工作目录,所以testdata/big.json这样的相对路径可以正常使用。
根目录中的其他文件
| 文件或目录 | 用途 |
|---|---|
go.mod、go.sum | 模块定义和依赖校验和,总是放在根目录 |
README.md、LICENSE | 和任何项目一样 |
Makefile 或 Taskfile.yml | 可选的构建快捷方式 |
Dockerfile | 对于服务,通常放在根目录 |
migrations/、web/、docs/ | 非 Go 资源,按内容命名 |
tools.go | 固定工具依赖的旧方法;Go 1.24 用 go.mod 中的 tool 行(go get -tool)取代了它 |
go.work | 在本地同时开发多个模块用的工作区;单模块项目通常不提交它 |
常见错误
- 给小项目套用庞大的模板。 从扁平结构开始,当出现第二个可执行文件或真正的边界时再添加目录。
- 名为
utils或common的包。 按包的功能给它命名。 - 每个文件或每个类型一个包。 Go 的包本来就比 Java 的类更大。一个包有十个文件很正常。
- 循环导入。 两个包不能互相导入。这通常说明它们本该放在一起,或者某个共用类型应该移到更底层的包里,或者其中一方应该依赖一个小接口,而不是另一个包。
- 条件反射式地使用
pkg/。 它给每个导入路径都加了一段,却没有增加任何含义。 - 把测试放在单独的目录中。 在那里测试看不到未导出的代码,工具链也不这样预期。
常见问题
Go 有官方的项目布局吗?
没有强制性的布局。Go 团队在 go.dev/doc/modules/layout 发布了指南,介绍了几种常见的形态(单个包、单个命令、带 internal/ 的多个命令)。流行的 golang-standards/project-layout 仓库是一个社区项目,并不是 Go 的标准,Go 团队也公开说明过这一点。
Go 中的 internal 目录是什么?
导入路径中包含 internal 一段的包,只能被以该 internal 目录的父目录为根的代码导入。example.com/app/internal/store 可以在 example.com/app/ 下的任何地方导入,而 go 命令会拒绝来自其他模块的导入,并报 use of internal package ... not allowed。
Go 项目中应该使用 pkg 目录吗?
这是可选的,它给路径多加了一段,却没有增加任何含义。一些大型项目用 pkg/ 把公开的库代码和其他代码分开,但 Go 标准库和大多数现代项目都不这么做。可导入的包放在模块根目录或有明确名字的目录中,私有的代码放进 internal/。
Go 项目中的测试文件放在哪里?
和被测代码放在同一个目录,命名为 *_test.go。Go 没有单独的测试目录树。测试用的数据文件放在测试旁边的 testdata 目录中,go 工具在查找包时会忽略它。