2. Go 项目结构演进:从 MVC 到整洁架构、六边形架构、洋葱架构在 Go 中的实践

Go 语言在业务项目中的结构演进,本质上是关注点分离与依赖方向控制不断强化的过程。早期 Go 项目多借鉴 Web 框架的 MVC 分层,随着业务复杂度上升,团队开始引入整洁架构、六边形架构和洋葱架构来隔离领域逻辑与基础设施。本章通过对比四种架构模式,展示 Go 项目结构的演进路径与落地要点。

2.1 经典 MVC 在 Go 中的局限

MVC(Model-View-Controller)是最早被 Go Web 框架(如 Beego、Gin)广泛采用的分层模式。典型目录结构如下:

myapp/
├── models/       # 数据模型与数据库操作
├── views/        # 模板文件(HTML/JSON)
├── controllers/  # 请求处理与路由
├── routes/       # 路由注册
└── main.go

这种结构在小型项目中快速有效,但进入业务密集期后暴露出三个核心问题:

  • 模型层膨胀:models 目录同时包含数据定义、校验、持久化、业务规则,单一文件可达数千行。
  • 循环依赖:controllers 引用 models,models 有时反向引用 controllers 或 utils,导致编译失败。
  • 测试困难:业务逻辑与数据库、HTTP 请求强耦合,单元测试必须启动外部依赖。
演进驱动力:当 models 目录超过 10 个文件且出现跨模型业务规则时,MVC 的扁平分层已无法支撑领域复杂度,必须引入更严格的边界。

2.2 整洁架构(Clean Architecture)在 Go 中的落地

整洁架构由 Robert C. Martin 提出,核心原则是依赖反转:内层(领域层)定义接口,外层(基础设施层)实现接口。Go 的接口机制天然适配这一模式。

典型目录结构:

myapp/
├── domain/               # 领域层(最内层)
│   ├── entity/           # 实体与值对象
│   ├── repository/       # 仓储接口
│   └── service/          # 领域服务
├── application/          # 应用层(用例层)
│   ├── dto/              # 数据传输对象
│   ├── port/             # 输入/输出端口接口
│   └── usecase/          # 用例实现
├── infrastructure/       # 基础设施层(最外层)
│   ├── persistence/      # 数据库实现
│   ├── web/              # HTTP 处理器
│   └── config/           # 配置读取
└── cmd/                  # 程序入口
    └── server/main.go
Go 实践要点:domain 层不导入任何外部包(仅依赖标准库);application 层只依赖 domain 层;infrastructure 层依赖 domain 和 application 层。所有依赖方向指向内层。

以下 SVG 展示了整洁架构的依赖方向与层间边界:

整洁架构(Clean Architecture)依赖方向 领域层 Entity / Value Object Repository Interface 应用层 Use Case / Port / DTO 依赖领域层接口 基础设施层 Persistence / Web / Config 实现领域层接口,依赖应用层 依赖方向:基础设施层 → 应用层 → 领域层(内层不感知外层) Go 接口定义在 domain 层,实现放在 infrastructure 层,编译期保证无循环依赖

2.3 六边形架构(Hexagonal Architecture)的端口-适配器模式

六边形架构由 Alistair Cockburn 提出,强调对称性:应用核心通过“端口”(Port)与外界通信,每种外部技术对应一个“适配器”(Adapter)。Go 中端口通常定义为 interface,适配器为 struct 实现。

目录结构示例:

myapp/
├── core/                  # 应用核心(六边形内部)
│   ├── domain/            # 领域模型
│   ├── port/              # 输入端口(驱动端)与输出端口(被驱动端)
│   │   ├── in/            # 用例接口(如 CreateOrderUseCase)
│   │   └── out/           # 仓储接口(如 OrderRepository)
│   └── service/           # 核心业务实现
├── adapter/               # 适配器层(六边形外部)
│   ├── in/                # 驱动适配器(HTTP/gRPC/CLI)
│   │   ├── rest/          # REST 处理器
│   │   └── grpc/          # gRPC 服务
│   └── out/               # 被驱动适配器(DB/消息队列/外部API)
│       ├── mysql/         # MySQL 实现
│       └── redis/         # Redis 实现
└── cmd/                   # 启动入口
概念 Go 实现方式 示例
输入端口(Inbound Port) interface 定义用例方法 type CreateOrderUseCase interface { Execute(ctx, req) (Order, error) }
输出端口(Outbound Port) interface 定义仓储方法 type OrderRepository interface { Save(ctx, Order) error }
输入适配器(Inbound Adapter) struct 实现端口,注入核心服务 type OrderHandler struct { useCase port.CreateOrderUseCase }
输出适配器(Outbound Adapter) struct 实现端口,封装具体技术 type MysqlOrderRepo struct { db *sql.DB }
常见误区:将 adapter 层中的 struct 直接暴露给 core 层。正确做法是 core 层只依赖 port 接口,adapter 层在启动时通过依赖注入将实现注入到 core 层。

2.4 洋葱架构(Onion Architecture)在 Go 中的变体

洋葱架构由 Jeffrey Palermo 提出,与整洁架构高度相似,但更强调领域模型为中心和显式的应用服务层。Go 社区常将其简化为三层:领域层、应用服务层、基础设施层。

Go 中的洋葱架构目录:

myapp/
├── domain/                # 最内层:领域模型与领域服务
│   ├── model/             # 聚合根、实体、值对象
│   ├── service/           # 领域服务(无状态业务逻辑)
│   └── spec/              # 规约模式(Specification)
├── application/           # 中间层:应用服务与用例编排
│   ├── service/           # 应用服务(事务、权限、编排)
│   ├── dto/               # 请求/响应 DTO
│   └── assembler/         # 模型与 DTO 转换
├── interfaces/            # 外层:接口适配器
│   ├── api/               # HTTP/gRPC 处理器
│   ├── repository/        # 仓储实现
│   └── event/             # 事件发布/订阅
└── infrastructure/        # 最外层:技术基础设施
    ├── db/                # 数据库连接与迁移
    ├── cache/             # 缓存客户端
    └── config/            # 配置管理

以下 SVG 对比三种架构的层间关系与依赖方向:

三种架构对比:依赖方向与层数 整洁架构 领域层 实体 / 接口 应用层 用例 / DTO 基础设施层 DB / HTTP 六边形架构 核心 领域+端口 输入适配器 HTTP / gRPC 输出适配器 MySQL / Redis 洋葱架构 领域模型 实体 / 值对象 领域服务 业务规则 应用服务 用例编排 基础设施 DB / 外部API 三种架构均遵循依赖反转原则:外层依赖内层,内层定义接口,外层提供实现

2.5 架构选型对比与 Go 项目推荐

维度 MVC 整洁架构 六边形架构 洋葱架构
层数 3 层 3-4 层 2 层(核心+适配器) 3-4 层
依赖方向 Controller→Model 外层→内层 适配器→核心 外层→内层
接口定义位置 无显式接口 领域层 核心层 port 包 领域层
测试隔离性 低(需 mock 数据库) 高(接口 mock) 高(端口 mock) 高(接口 mock)
Go 项目适用规模 < 5 万行 5-50 万行 10-100 万行 5-30 万行
学习曲线 低 中 中高 中
Go 项目推荐路径:
小型项目(< 3 万行):使用扁平 MVC 或简化版整洁架构(domain + handler + repo)。
中型项目(3-20 万行):采用六边形架构,端口定义清晰,适配器可替换。
大型项目(> 20 万行):使用整洁架构或洋葱架构,严格分层,领域层零外部依赖。

2.6 从 MVC 迁移到领域驱动架构的步骤

  1. 识别核心领域:从 controllers 和 models 中提取不依赖 HTTP/DB 的业务逻辑,放入 domain 包。
  2. 定义端口接口:在 domain 层创建 repository 和 service 接口,方法签名只使用领域类型。
  3. 拆分应用层:将 controllers 中的编排逻辑移到 application/usecase 包,只保留 HTTP 解析与响应。
  4. 实现适配器:将原有的 models 数据库操作拆分为独立的 repository 实现,注入到 usecase。
  5. 依赖注入:在 main.go 或 wire 文件中组装依赖,确保 domain 层不直接实例化基础设施对象。
迁移避坑:不要一次性重构所有模块。建议按业务边界(Bounded Context)逐个迁移,每个上下文独立验证测试通过后再推进下一个。保持 main.go 作为唯一的组装点,避免在多个文件中分散依赖注入逻辑。