后端架构分层设计:Controller、Service 与 Repository

FreeGuideOnline 最新 2026-06-16

为什么需要分层架构

后端应用在业务逻辑复杂化后,所有代码都揉在一起会导致可读性塌缩、维护成本飙升。分层架构通过按职责分离代码,把“接收请求”“处理业务”“读写数据”拆分到不同层,每一层只做一件事,边界清晰。最常见的三层结构是:Controller(接口层)、Service(业务逻辑层)、Repository(数据访问层)。本教程将带你把一个混沌的单体函数一步步拆分成清晰可测试的分层模块,并使用 Go 语言给出简洁可运行的示例。


三层架构核心职责

Controller:接口层的唯一工作

Controller 是系统的入口,它负责:

  • 解析 HTTP 参数(路径参数、查询参数、请求体)
  • 校验参数格式(是否为空、数值范围等)
  • 调用对应的 Service 方法
  • 将 Service 返回的数据转换成 HTTP 响应(JSON、状态码)

Controller 绝不包含任何业务逻辑:它像一个“翻译官”,把 HTTP 协议的语言转成内部函数调用,再把结果转回 HTTP 协议。

// ❌ 错误示例:Controller 里直接拼接 SQL
func GetOrder(c *gin.Context) {
    id := c.Param("id")
    db.Query("SELECT * FROM orders WHERE id = ?", id) // 严重违规
}

// ✅ 正确示例:Controller 只做转发
func GetOrder(c *gin.Context) {
    id := c.Param("id")
    order, err := orderService.GetByID(id)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, order)
}

Service:所有业务的栖息地

Service 层是系统的灵魂,承载全部业务规则和用例逻辑。它:

  • 调用 Repository 获取数据
  • 执行校验、计算、聚合、状态转换
  • 编排多个 Repository 操作或外部服务调用(如发邮件、扣库存)
  • 返回处理结果给 Controller

Service 不应该知道 HTTP 的存在,它的输入输出都是普通结构体,不依赖任何 Web 框架。这样 Service 可以单独进行单元测试,甚至可以复用到命令行工具或事件处理器中。

type OrderService struct {
    repo OrderRepository
}

func (s *OrderService) GetByID(id string) (*Order, error) {
    if id == "" {
        return nil, errors.New("订单ID不能为空")
    }
    return s.repo.FindByID(id)
}

Repository:数据访问的封装

Repository 层负责和数据库(或任何持久化存储)打交道,它:

  • 实现数据持久化操作(增删改查)
  • 隐藏 SQL、ORM 细节
  • 只返回领域对象,不暴露数据库实现细节

Repository 不处理业务逻辑,它只是一个数据映射器,让 Service 可以用“集合”的思维操作数据。

type OrderRepository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

type mysqlOrderRepo struct {
    db *gorm.DB
}

func (r *mysqlOrderRepo) FindByID(id string) (*Order, error) {
    var order Order
    err := r.db.Where("id = ?", id).First(&order).Error
    if err != nil {
        return nil, err
    }
    return &order, nil
}

依赖方向:高层不依赖低层实现

分层的核心原则之一就是依赖倒置:Service 层应该依赖 Repository 的接口,而不是具体实现。这样当你想把 MySQL 换成 PostgreSQL 或者内存存储时,只需要新建一个实现了相同接口的 Repository,Service 代码零修改。

// Service 持有接口,而非具体类型
type OrderService struct {
    repo OrderRepository // 接口
}

Controller 同样只依赖 Service 接口,采用依赖注入的方式将具体实例传入。典型的初始化顺序:

main → 创建数据库连接 → 创建 Repository 实例 → 创建 Service 实例 → 创建 Controller 实例 → 注册路由

一个完整的三层架构示例

下面是一个“用户下单”的简化实现,展示各层如何配合。

领域模型

type Order struct {
    ID     string
    UserID string
    Amount float64
    Status string
}

Repository 接口与实现

type OrderRepository interface {
    Save(order *Order) error
}

type InMemoryOrderRepo struct {
    data map[string]*Order
}

func (r *InMemoryOrderRepo) Save(order *Order) error {
    r.data[order.ID] = order
    return nil
}

Service 层

type OrderService struct {
    repo OrderRepository
}

func NewOrderService(repo OrderRepository) *OrderService {
    return &OrderService{repo: repo}
}

func (s *OrderService) CreateOrder(userID string, amount float64) (*Order, error) {
    if amount <= 0 {
        return nil, errors.New("金额必须大于0")
    }
    order := &Order{
        ID:     generateID(),
        UserID: userID,
        Amount: amount,
        Status: "created",
    }
    err := s.repo.Save(order)
    if err != nil {
        return nil, err
    }
    return order, nil
}

Controller 层(Gin 示例)

func CreateOrderHandler(service *OrderService) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req struct {
            UserID string  `json:"user_id"`
            Amount float64 `json:"amount"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": "参数格式错误"})
            return
        }
        order, err := service.CreateOrder(req.UserID, req.Amount)
        if err != nil {
            c.JSON(422, gin.H{"error": err.Error()})
            return
        }
        c.JSON(201, order)
    }
}

组装与启动

func main() {
    repo := &InMemoryOrderRepo{data: make(map[string]*Order)}
    service := NewOrderService(repo)
    router := gin.Default()
    router.POST("/orders", CreateOrderHandler(service))
    router.Run()
}

常见分层误区与解答

误区一:Service 层太薄,只是透传 Repository

如果 Service 里只有一行 return s.repo.Find(id),那这个 Service 就退化成“装饰器”。这种情况下,可以让 Controller 直接调用 Repository 吗?不建议。因为哪怕今天没有业务逻辑,未来也可能会增加权限检查、缓存、日志等。Service 是预留的业务扩展点。

误区二:Controller 里做了太多业务判断

例如在 Controller 里判断“如果用户余额不足就返回 400”。余额判断是典型业务规则,应该在 Service 里实现,Controller 只负责根据 Service 返回的错误类型映射 HTTP 状态码。

误区三:跨层传参数直接使用框架对象

gin.Context 从 Controller 一直传到 Service 甚至 Repository,会导致 Service 被 Web 框架绑定,无法在其他环境复用。应当立即提取出需要的数据,以普通参数形式向下传递。


分层架构如何让测试变简单

三层架构最直接的好处就是可测试性大幅提升。你可以为每一层编写独立的单元测试,无需启动 HTTP 服务器或真实数据库。

func TestCreateOrder(t *testing.T) {
    repo := &InMemoryOrderRepo{data: make(map[string]*Order)}
    service := NewOrderService(repo)
    order, err := service.CreateOrder("user1", 99.9)
    assert.NoError(t, err)
    assert.Equal(t, "created", order.Status)

    // 直接验证仓储数据
    saved, _ := repo.FindByID(order.ID)
    assert.NotNil(t, saved)
}

对于 Controller,可以用 httptest 模拟 HTTP 请求,结合 mock 的 Service 来测试状态码和响应体,真正实现航空级安全覆盖。


进阶:何时需要引入更细的分层

当应用进一步膨胀,可以考虑以下扩展:

  • DTO / VO 层:独立定义 Controller 返回给前端的结构体,避免直接暴露领域模型,保护数据隐私。
  • Facade 层:多个 Service 组成的复杂编排,提供粗粒度用例接口。
  • Event / Listener 层:分离核心业务和副作用(如发送通知),通过事件驱动解耦。

但原则始终不变:每一层只增加有意义的抽象,不过早优化。初学阶段深刻掌握 Controller - Service - Repository 足以应对 80% 的后端开发场景,所有高级架构都是在此基础上按需生长。


小结

分层架构的核心不是把代码放进不同的文件夹,而是建立清晰的调用边界与依赖规则。Controller 解析请求、Service 处理业务、Repository 读写数据,三层单向依赖,接口隔离实现。立刻打开你的项目,找出一个接口里混杂了 SQL 查询和 if-else 的 handler,试着用本教程的方法重构它——你会发现,代码变得像乐高一样灵活易拼插。