写 Go 项目,最头疼的事情不是语法——语法一天就能上手。真正让人纠结的是"代码往哪放"。
Go 官方不像 Rails 或 Django 那样给你规定目录结构。你搜"Go project layout",能搜出来一堆方案,什么 golang-standards/project-layout、clean architecture、hexagonal architecture……每个都有道理,但每个都不能直接拿来就用。
我也纠结过一阵。最早写 Go 就一个main.go打天下,后来项目大了开始拆文件夹,拆的过程中踩了不少坑——层太多了写起来烦,层太少了改起来痛。折腾了几个项目之后,慢慢沉淀出一套自己用着顺手的结构。
不敢说这是最优解,但至少是在实际项目里磨合过的。这篇就聊聊这套结构长什么样,以及每个设计决策背后的想法。
整体目录结构
先把全貌亮出来:
cmd/ api/ API 服务入口 worker/ 异步队列消费者入口 migrate/ 数据库迁移入口 config/ 环境变量加载与类型化配置 internal/ server.go HTTP 服务装配 worker.go Worker 服务装配 bootstrap/ 进程级资源初始化和关闭 errcode/ 错误码定义 handler/ HTTP handler service/ 业务逻辑 repository/ 数据访问层 model/ 数据模型 middleware/ Gin 中间件 router/ 路由注册 task/ 异步任务定义 taskqueue/ 异步队列边界 worker/ 异步队列运行时组件 pkg/ auth/ JWT 鉴权 cache/ Redis 封装 database/ 数据库初始化 log/ 日志初始化 response/ 统一 JSON 响应 validator/ 参数校验 tests/ integration/ PostgreSQL、Redis、Asynq 集成测试 .github/ workflows/ ci.yml 自动化测试、lint 和容器验证 Dockerfile 三个进程入口的多阶段镜像构建 .dockerignore Docker 构建上下文排除规则 compose.yaml 本地完整依赖栈与启动顺序 Makefile 开发、验证和容器命令入口几个顶层目录的职责:
- cmd/:进程入口。每种进程一个子目录,
main.go只负责进程级编排:API 和 Worker 管理启动与关闭,Migrate 执行一次性迁移后退出 - config/:只负责从环境变量加载类型化配置,不在这里创建数据库、Redis 或 JWT
- internal/bootstrap/:初始化 API 和 Worker 的共享进程资源,组装 Registry,并统一负责关闭;Migrate 作为一次性入口直接管理自己的数据库连接
- internal/:模块私有的应用代码、进程装配和运行时组件。Go 的
internal约定保证这些包不会被外部项目直接引用 - pkg/:基础设施辅助包。大部分保持业务无关,少数包(例如统一响应)仍服务于当前应用契约
- tests/integration/:连接真实 PostgreSQL 和 Redis,并通过 Asynq client 验证任务持久化;使用独立 build tag 与默认单元测试隔离
- 根目录工程文件:
Dockerfile、Compose、Makefile和 GitHub Actions 共同负责本地运行、构建与自动化验证,不承载业务逻辑
这不是照搬 golang-standards/project-layout。那个仓库里有些目录我觉得没必要(比如api/、third_party/),有些是我后来根据实际需要加的(比如拆了三个 cmd)。项目结构这种东西,合适比"标准"重要。