关于 go-project-layout
go-project-layout 是面向 Go 开发者和编码 Agent 的项目结构技能,根据规模与类型提供布局建议,并涵盖包边界、依赖方向、配置和验证。
主要功能
- 01按项目规模选择布局
- 02模块与包命名规则
- 03服务和库的目录结构
- 04依赖方向原则
- 05配置与验证清单
适用场景
- 01搭建小型 Go 工具
- 02规划 Go 服务结构
- 03组织可导入的 Go 库
- 04确定模块边界
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-project-layout安装后在 Claude Code 中输入/go-project-layout确认已加载。
详细介绍
Go 项目布局
结构应与项目规模相匹配。Go 项目中最常见的布局错误,是为只有 500 行代码的工具照搬微服务骨架,或者把一个包含 50 个包的服务都塞进扁平目录。布局应与项目相匹配。
社区默认规则。若项目技能或 Agent 配置规则明确取代了
go-project-layout,则以该规则为准。
1. 根据项目规模选择布局
| 项目 | 布局 |
|---|---|
| 小型工具、单个二进制文件、少于 5 个文件 | 扁平布局:所有内容都放在根目录的 main 包中 |
| 供他人导入的库 | 根包以模块命名,辅助代码放在 internal/ 中 |
| 只有一个二进制文件的服务 | cmd/<name>/main.go + internal/ 包 |
| 多个共享代码的二进制文件 | cmd/<name1>/、cmd/<name2>/ + internal/ |
切勿为了“以后用得上”而预先创建空的 pkg/、api/、docs/、build/ 目录。应在代码需要时再添加结构,而不是提前添加。
2. 模块命名
# ✅ Good — repository path, lowercase
go mod init github.com/acme/payment-service
# ❌ Bad — not fetchable, uppercase, or vanity without DNS
go mod init PaymentService
go mod init payment_service
路径的最后一部分应与用户看到的名称一致:对于库,它会成为默认的导入名。
3. 服务布局(API 和工作进程的默认布局)
payment-service/
├── cmd/
│ └── payment-api/
│ └── main.go # flag/env parsing, wiring, Run() — nothing else
├── internal/
│ ├── domain/ # core types, business rules; zero external deps
│ ├── service/ # use cases orchestrating domain + stores
│ ├── store/ # data access implementations (postgres/, redis/)
│ ├── handler/ # HTTP/gRPC adapters
│ └── config/ # config loading and validation
├── migrations/ # if the service owns a database
├── go.mod
├── Makefile
└── README.md
规则:
- 默认使用
internal/——编译器会确保模块外部的代码无法导入其中的内容。只有在确有需要时才将其提升为公开包。 - 仅当存在外部使用方且模块也包含私有代码时才使用
pkg/。拿不准时就不要创建它。 - 依赖关系应指向内部:
handler → service → domain ← store。domain不导入store或handler。
4. 保持 main 精简,并让 Run 可运行
让 main.go 只负责组装依赖并委托调用,这样应用才能方便测试:
func main() {
if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, getenv func(string) string) error {
cfg, err := config.Load(getenv)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
db, err := store.Open(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
svc := service.New(store.NewUserRepo(db))
srv := handler.NewServer(cfg.Addr, svc)
return srv.ListenAndServe(ctx)
}
os.Exit只出现一次,且位于main中。run接收其依赖项(args、getenv),以便测试调用它。- 不要使用
init()函数组装依赖——只采用显式的构造顺序。
5. 库的布局
retry/
├── retry.go # package retry — the API, in the root
├── retry_test.go
├── backoff.go # same package, split by topic
├── internal/
│ └── clock/ # implementation details users must not import
├── examples_test.go # Example* functions shown in godoc
└── go.mod
- 根目录本身就是包。不要使用
src/或lib/。 - 每个概念对应一个包。避免使用
util、common、helpers这类名称——应按包所提供的功能命名(retry、clock、httpsign)。
6. 目录和包的命名规则
- 包名应与目录名一致,简短、小写且不带下划线:使用
store/postgres,而不是store/postgres_impl。 - 不要重复表达:使用
payment.Service,而不是payment.PaymentService。 cmd/中的二进制名称面向用户:cmd/payment-api;目录名可使用连字符(目录中只放main包)。
7. 应放在根目录的文件
go.mod、go.sum、README.md、LICENSE、Makefile、.golangci.yml、Dockerfile(单二进制项目)。- 请勿创建:
src/(不符合惯例)、vendor/(除非团队明确要求将依赖 vendored)、以及诸如types/或models/这类最终会变成杂物堆的单文件包。
8. 配置
- 使用一个配置结构体,在一个位置(
internal/config)从环境中加载,并在启动时校验——一次列出所有缺失或无效的值,而不是只报告第一个。 - CLI 使用标志;服务使用环境变量(12 因素);默认值写在代码中。不要将配置文件打包进镜像,也不要在业务代码中编写针对特定环境的分支(
if env == "prod")。 - 密钥应来自环境或密钥管理器,绝不能来自标志(
ps会显示 argv)或代码仓库。
9. 使用一个模块还是多个模块
默认每个代码仓库使用一个模块。只有在不同部分需要独立进行版本管理和发布时才拆分模块(例如,服务旁边的公开客户端库)。使用 go.work 在本地跨模块开发;不要提交 replace => ../path 指令。
脚手架搭建流程
- 提出问题并作出决定:这是工具、库还是服务?有多少个二进制文件?
- 执行
go mod init <repo-path>,然后有意设置go指令 — 从 Go 1.26 起,go mod init写入的是上一版本 (go 1.(N-1).0),而不是工具链的版本。 - 只创建首个功能需要的目录。
- 按照上文的精简主函数模式编写
main.go,并创建配置结构体。 - 将开发工具固定为模块工具(Go 1.24+ 使用
go get -tool ...),并在Makefile中添加build、test、lint目标。 - 验证:脚手架上的
go build ./...和go vet ./...均能通过。
验证清单
- 目录布局与项目规模相符 — 不要创建空的脚手架目录
- 模块路径是可获取的仓库路径
- 所有非公开包都位于
internal/下 main.go保持精简:解析、组装、调用run、退出os.Exit只在main中使用;不在init()中组装依赖- 依赖向内流动;
domain不导入任何基础设施包 - 不要创建
util/common/helpers/models这类杂项包 - 包名与目录名一致,使用小写,避免名称重复
- 新建的脚手架上
go build ./...能通过 - 在启动时于单一位置加载并验证配置;不要在标志中放入密钥
- 除非各部分独立发布,否则只使用一个模块;不要提交本地
replace
评论
支持 Markdown