2. Go 语言项目结构基础:标准 Go 项目布局与 DDD 准备
在云原生与微服务架构中,项目结构不仅是代码的物理组织方式,更是团队协作、部署策略和领域建模的基石。本章将深入剖析 Standard Go Project Layout 的核心原则,明确 cmd、internal、pkg 三大目录的职责边界,并在此基础上为引入领域驱动设计(DDD)做好结构准备。
2.1 标准 Go 项目布局全景
Go 官方并未强制规定项目布局,但社区经过多年实践形成了广泛认可的 Standard Go Project Layout。其核心思想是:按职责分层,按边界隔离。以下是一个典型的生产级项目骨架:
my-go-project/
├── cmd/ # 应用入口(main 包)
│ ├── api/ # HTTP/gRPC 服务入口
│ │ └── main.go
│ └── worker/ # 后台任务入口
│ └── main.go
├── internal/ # 私有应用逻辑(禁止外部导入)
│ ├── domain/ # 领域模型与业务规则
│ ├── service/ # 应用服务(用例编排)
│ └── repository/ # 数据持久化实现
├── pkg/ # 可复用的公共库
│ ├── logger/ # 日志封装
│ └── config/ # 配置加载
├── api/ # API 定义(proto / OpenAPI)
├── configs/ # 配置文件模板
├── scripts/ # 构建与部署脚本
├── test/ # 外部集成测试
├── go.mod
└── go.sum
关键原则:
cmd 只做依赖注入和启动编排,不包含业务逻辑;internal 是应用的核心堡垒,禁止外部包直接引用;pkg 是经过抽象的可复用工具,应保持无业务语义。
2.2 三大核心目录的职责划分
| 目录 | 职责 | 导入限制 | 典型内容 |
|---|---|---|---|
cmd/ |
应用入口点,每个子目录对应一个可执行二进制 | 只能导入 internal 和 pkg |
main() 函数、依赖注入、服务器启动 |
internal/ |
私有应用逻辑,Go 编译器强制禁止外部导入 | 仅限本项目内部使用 | 领域模型、用例、仓储实现、中间件 |
pkg/ |
可对外共享的通用库,无业务上下文 | 可被外部项目导入 | 日志、配置、HTTP 客户端、工具函数 |
常见陷阱: 将业务逻辑直接写在
cmd/ 的 main.go 中,或将领域模型放在 pkg/ 下导致外部依赖泄漏。记住:cmd 是薄薄的胶水层,pkg 不应包含任何业务实体。
2.3 从分层到领域:为 DDD 做准备
标准 Go 项目布局天然支持 分层架构,但要演进到 DDD,需要在 internal/ 内部进一步划分领域边界。下图展示了从传统分层到 DDD 四层架构的映射关系:
2.4 internal/domain:领域模型的核心阵地
在 DDD 准备阶段,internal/domain 是最重要的目录。它应该包含:
- 实体(Entity):具有唯一标识的业务对象,如
User、Order - 值对象(Value Object):不可变、无标识的业务概念,如
Money、Address - 领域服务(Domain Service):跨多个实体或值对象的业务逻辑
- 仓储接口(Repository Interface):定义持久化契约,不包含实现
- 领域事件(Domain Event):业务状态变更的声明
实践要点: domain 层应零外部依赖——不导入任何框架、数据库驱动或 HTTP 库。它只依赖 Go 标准库和自身定义的接口。这是 DDD 架构的“黄金法则”。
2.5 internal/service:应用服务的编排层
internal/service 对应 DDD 的 应用层,负责:
- 接收来自
cmd/或 handler 的请求 - 调用 domain 层的实体和领域服务执行业务规则
- 通过 repository 接口持久化数据
- 返回结果或 DTO(数据传输对象)
一个典型的应用服务示例:
// internal/service/order_service.go
package service
import (
"context"
"my-project/internal/domain"
)
type OrderService struct {
orderRepo domain.OrderRepository
paymentSvc domain.PaymentService
}
func NewOrderService(repo domain.OrderRepository, ps domain.PaymentService) *OrderService {
return &OrderService{orderRepo: repo, paymentSvc: ps}
}
func (s *OrderService) CreateOrder(ctx context.Context, userID string, items []domain.OrderItem) (*domain.Order, error) {
// 1. 领域层创建订单实体
order, err := domain.NewOrder(userID, items)
if err != nil {
return nil, err
}
// 2. 调用领域服务处理支付
if err := s.paymentSvc.Process(ctx, order); err != nil {
return nil, err
}
// 3. 仓储持久化
if err := s.orderRepo.Save(ctx, order); err != nil {
return nil, err
}
return order, nil
}
2.6 pkg/ 与 internal/ 的边界决策
很多团队在 pkg/ 和 internal/ 之间犹豫不决。以下决策矩阵可帮助判断:
| 判断条件 | 放入 pkg/ | 放入 internal/ |
|---|---|---|
| 是否包含业务语义 | 否(如日志、配置、HTTP 客户端) | 是(如订单、用户、支付模型) |
| 是否被外部项目复用 | 是(需保持向后兼容) | 否(仅限本项目) |
| 变更频率 | 低(稳定接口) | 高(随业务迭代) |
| 测试策略 | 单元测试为主 | 单元 + 集成测试 |
2.7 本章小结:结构即架构
标准 Go 项目布局不是教条,而是经过验证的 架构边界表达方式。通过 cmd/internal/pkg 的清晰划分,我们为后续的 DDD 演进、容器化部署和 CI/CD 流水线奠定了坚实的结构基础。记住:好的项目结构让领域边界可见,让部署单元可独立演进。
下一步行动: 在开始编码前,先画出你的领域边界图,然后按照
cmd → internal/domain → internal/service → internal/repository 的顺序创建目录结构。让结构驱动实现,而非让实现随意生长。