Handler-Service-Repository 分层
Go 后端的请求处理分层设计:handler 负责协议转换、service 负责业务规则、repository 负责数据存取。依赖方向单向,每层可独立测试。
#type / concept
#status / growing
#tech / dev / backend
#resource / go
[!info] related notes
- 前置: Go 项目结构与分层
- 依赖注入: Go 手动依赖注入
- 框架: Gin HTTP 框架
- 数据访问: gorm
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
}