Handler-Service-Repository 分层

Go 后端的请求处理分层设计:handler 负责协议转换、service 负责业务规则、repository 负责数据存取。依赖方向单向,每层可独立测试。

#type / concept #status / growing #tech / dev / backend #resource / go

[!info] related notes

Handler-Service-Repository 分层

核心问题

一个 HTTP 请求的处理涉及:解析请求、验证业务规则、操作数据库、格式化响应。如果不分层,这些逻辑混在一个函数里,无法单独测试、无法复用、换数据库要改所有代码。

三层职责

Handler(协议层)

只做:HTTP 请求/响应的格式转换

func (h *Handler) CreateSession(c *gin.Context) {
    // 1. 从 HTTP 提取数据
    userID, _ := c.Get("user_id")
    uid, _ := uuid.Parse(userID.(string))

    // 2. 调用 service
    session, err := h.service.CreateSession(c.Request.Context(), uid)
    if err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    // 3. 格式化响应
    c.JSON(201, session)
}

不该做:业务判断、直接操作数据库

Service(业务层)

只做:业务规则和流程编排

func (s *Service) CreateSession(ctx context.Context, userID uuid.UUID) (*Session, error) {
    // 业务规则:已有进行中的空会话就复用
    existing, _ := s.repo.GetLastInProgressEmpty(ctx, userID)
    if existing != nil {
        return existing, nil
    }

    session := &Session{ID: uuid.New(), UserID: userID, Status: "in_progress"}
    return session, s.repo.Create(ctx, session)
}

不该做:知道 HTTP 协议、直接操作数据库

Repository(数据层)

只做:数据库操作的封装

func (r *Repo) Create(ctx context.Context, session *Session) error {
    return r.db.WithContext(ctx).Create(session).Error
}

func (r *Repo) GetByID(ctx context.Context, id uuid.UUID) (*Session, error) {
    var s Session
    err := r.db.WithContext(ctx).Where("id = ?", id).First(&s).Error
    if err == gorm.ErrRecordNotFound {
        return nil, nil  // 不暴露 ORM 特定错误
    }
    return &s, err
}

不该做:包含业务规则、返回框架特定错误

依赖方向

handler → service → repository → database

严格单向,不能反过来。Service 通过接口引用 Repository:

type sessionRepository interface {
    Create(ctx context.Context, session *Session) error
    GetByID(ctx context.Context, id uuid.UUID) (*Session, error)
}

type Service struct {
    repo sessionRepository  // 接口,不是具体实现
}

好处:测试时可以用 mock,换实现不需要改 service。

测试策略

// 测试 Service 层,不需要数据库
type mockRepo struct{}
func (m *mockRepo) Create(ctx context.Context, s *Session) error { return nil }

func TestCreateSession_ReusesExisting(t *testing.T) {
    svc := NewService(&mockRepo{})
    session, err := svc.CreateSession(context.Background(), uuid.New())
    assert.NoError(t, err)
    assert.Equal(t, "collecting", session.Phase)
}

DTO 转换

DTO(请求/响应结构体)与数据库 Model 解耦:

// Handler 层用 DTO
type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

// Repository 层用 Model
type User struct {
    ID           uuid.UUID
    Email        string
    PasswordHash string  // 永远不出现在 DTO 中
}

常见错误

Handler 里写业务逻辑

// ❌ 业务规则写在 handler 里
func (h *Handler) Create(c *gin.Context) {
    existing := h.repo.GetLastEmpty(userID)  // 直接调 repo
    if existing != nil { ... }
}

Service 直接用 gorm.DB

// ❌
type Service struct {
    db *gorm.DB  // 应该用 repository 接口
}

Repository 返回框架错误

// ❌
return &session, err  // 调用方需要判断 gorm.ErrRecordNotFound

// ✅
if err == gorm.ErrRecordNotFound {
    return nil, nil
}
创建于 2026/6/25 更新于 2026/6/25