3. 分层架构基础:经典三层架构
经典三层架构是Go后端项目中最广泛使用的结构模式,它将系统划分为 Handler(接口层)、Service(业务层)、Repository(数据层) 三个垂直切面。每一层只关注本层职责,通过明确的依赖规则形成单向调用链,从而降低耦合、提升可测试性。
核心原则: 上层依赖下层,下层不感知上层。Handler 调用 Service,Service 调用 Repository,Repository 只操作数据源。任何反向依赖或跨层调用都视为架构违规。
3.1 各层职责定义
| 层次 | 别名 | 核心职责 | 典型产出 |
|---|---|---|---|
| Handler | 传输层 / 控制器 | 接收请求、参数校验、调用 Service、组装响应 | HTTP handler、gRPC endpoint、DTO |
| Service | 应用服务 / 用例 | 编排业务逻辑、事务管理、领域规则执行 | Service 接口与实现、业务错误码 |
| Repository | 数据访问层 | 持久化操作、查询构造、数据源适配 | Repository 接口、ORM/原生 SQL 实现 |
关键区分: Handler 不直接操作数据库,Service 不处理 HTTP 请求细节,Repository 不包含业务判断。每一层都保持“知识最小化”。
3.2 层间依赖规则
依赖方向严格遵循 单向依赖,且依赖的是接口而非具体实现。具体规则如下:
- Handler → Service:Handler 持有 Service 接口,通过依赖注入获得实现。
- Service → Repository:Service 持有 Repository 接口,不关心底层是 MySQL、Redis 还是 Mock。
- Repository → 数据源:Repository 实现依赖具体数据库驱动或 ORM,但接口定义在领域层。
- 禁止跨层调用:Handler 不能直接调用 Repository,Service 不能直接处理 HTTP 响应。
- 禁止循环依赖:A 层调用 B 层,B 层绝不能反向调用 A 层。
3.3 代码结构示例
以下是一个典型的Go项目目录结构,严格遵循三层分层:
project-root/
├── handler/
│ ├── user_handler.go // 定义 UserHandler,依赖 user.Service 接口
│ └── dto/
│ └── user_dto.go // 请求/响应结构体
├── service/
│ ├── user_service.go // 定义 UserService 接口
│ └── impl/
│ └── user_service_impl.go // 实现,依赖 user.Repository 接口
├── repository/
│ ├── user_repository.go // 定义 UserRepository 接口
│ └── mysql/
│ └── user_repo_mysql.go // MySQL 实现
└── domain/
└── user.go // 领域实体 User
常见陷阱: 很多团队将 Repository 直接放在
models/ 或 dao/ 包下,导致 Service 层直接依赖 ORM 模型。正确的做法是:Repository 接口定义在领域层,实现放在独立的 repository/mysql/ 包中,Service 只依赖接口。
3.4 层间数据流转
数据在层间传递时,每一层使用不同的数据结构,避免层间数据耦合:
- Handler → Service:使用 DTO(Data Transfer Object),只包含当前用例需要的数据。
- Service → Repository:使用领域实体或查询参数对象,不包含 HTTP 上下文。
- Repository → Service:返回领域实体或聚合根,不返回 ORM 模型。
- Service → Handler:返回业务结果或错误码,Handler 负责组装为响应格式。
3.5 分层架构的优势与局限
| 维度 | 优势 | 局限 |
|---|---|---|
| 可测试性 | 每层可独立 Mock 下层接口进行单元测试 | 层数增多时测试桩编写成本上升 |
| 可维护性 | 职责清晰,新人容易理解代码归属 | 简单 CRUD 场景下分层显得冗余 |
| 可替换性 | 替换数据源只需更换 Repository 实现 | 业务逻辑分散在 Service 层,容易膨胀 |
| 团队协作 | 前后端、不同模块可并行开发 | 跨层修改时涉及多个包,变更成本高 |
实践建议: 对于小型项目或微服务中的简单查询,允许 Handler 直接调用 Repository 的只读方法,但必须通过接口隔离,且业务逻辑仍保留在 Service 层。这种“简化三层”在 Go 社区中也被称为“瘦 Service 模式”。
3.6 依赖注入与层间解耦
Go 中实现层间依赖解耦的推荐方式是 构造函数注入:
// handler/user_handler.go
type UserHandler struct {
userSvc service.UserService // 依赖接口
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{userSvc: svc}
}
// service/impl/user_service_impl.go
type userServiceImpl struct {
userRepo repository.UserRepository // 依赖接口
}
func NewUserService(repo repository.UserRepository) service.UserService {
return &userServiceImpl{userRepo: repo}
}
// main.go 组装
func main() {
db := mysql.NewDB()
userRepo := mysql.NewUserRepo(db)
userSvc := impl.NewUserService(userRepo)
userHdl := handler.NewUserHandler(userSvc)
// 注册路由...
}
这种模式确保:
- 每一层只依赖接口,不依赖具体实现类。
- 在
main.go或独立的wire.go中完成组装,层与层之间没有硬编码依赖。 - 单元测试时可以直接传入 Mock 实现,无需启动数据库或 HTTP 服务。
进阶提示: 当项目规模增大后,建议引入 Google Wire 或 Uber Fx 等依赖注入框架,自动生成组装代码,避免手动维护
main.go 中的依赖链。