Go 四层架构实战:Handler + Service + Repository + Entity(清晰、可控、可演进)

📌 写在前面:

很多 Go 项目起步时"一把梭",半年后代码变成"意大利面条"。改需求像拆炸弹,加日志找不到头,测试写不下去。

**四层架构不是银弹,而是防御性编程的骨架。**它用明确的边界,让代码可维护、可测试、可替换。

本文不堆砌理论,只讲生产级落地方案:职责划分、依赖流向、完整代码示例、90%团队踩过的坑。


一、四层职责拆解(一句话讲清边界)

层级 职责 包含什么 禁止什么 依赖方向
Entity 承载业务状态与核心规则 纯结构体、值对象、业务枚举、验证逻辑 不含 HTTP/DB/日志/第三方依赖 零依赖(纯 Go)
Repository 数据访问抽象 接口定义、查询/持久化契约 不含业务逻辑、不控制事务 接口定义在使用方 ← 实现依赖 Entity
Service 业务逻辑编排 用例控制、事务管理、跨实体交互、业务校验 不解析 HTTP 请求、不拼 SQL 依赖 Repository 接口 + Entity
Handler 协议适配层 路由注册、参数解析、DTO 转换、响应格式化 不含业务逻辑、不碰数据库 依赖 Service

💡 架构师一句话:Handler 负责"说什么",Service 负责"做什么",Repository 负责"怎么存",Entity 负责"是什么"。


二、标准工作流与依赖流向

复制代码

HTTP Request

↓

Handler ← 解析请求、DTO→Entity 映射、调用 Service、格式化响应

↓

Service ← 业务校验、开启事务、调用 Repository 接口、处理业务错误

↓

Repository Interface ← 定义契约(放在 Service/Domain 层)

↓

Repository Impl ← pgx/gorm/es 具体实现,返回 Entity

↓

PostgreSQL / Cache / 外部 API

🔑 核心原则:依赖倒置(DIP)
Handler → Service → Repository Interface ← Repository Impl
接口永远定义在调用方,实现放在底层。 这是 Go 解耦的第一性原理。


三、完整实战代码(生产级可直接复用)

3.1 Entity:业务实体(纯结构体)

Go 复制代码
// entity/user.go

package entity

import "time"

type User struct {

    ID        int64     `json:"id"`

    Username  string    `json:"username"`

    Email     string    `json:"email"`

    Status    int8      `json:"status"` // 1:active, 0:disabled

    CreatedAt time.Time `json:"created_at"`

}

// 业务规则校验(放在实体内,保证核心不变量)

func (u *User) Validate() error {

    if u.Username == "" || len(u.Username) > 50 {

        return ErrInvalidUsername

    }

    if !isValidEmail(u.Email) {

        return ErrInvalidEmail

    }

    return nil

}

3.2 Repository:接口定义 + 实现

Go 复制代码
// service/user_repo.go ← 接口定义在调用方(Service层)

package service

import (

    "context"

    "yourproject/entity"

)

type UserRepository interface {

    GetByID(ctx context.Context, id int64) (*entity.User, error)

    Create(ctx context.Context, u *entity.User) error

}
Go 复制代码
// infra/postgres/user_repo.go ← 实现依赖 Entity

package postgres

import (

    "context"

    "errors"

    "fmt"

    "github.com/jackc/pgx/v5"

    "github.com/jackc/pgx/v5/pgxpool"

    "yourproject/entity"

    "yourproject/service"

)

type pgxUserRepo struct {

    pool *pgxpool.Pool

}

func NewUserRepo(pool *pgxpool.Pool) service.UserRepository {

    return &pgxUserRepo{pool: pool}

}

func (r *pgxUserRepo) GetByID(ctx context.Context, id int64) (*entity.User, error) {

    var u entity.User

    err := r.pool.QueryRow(ctx,

        `SELECT id, username, email, status, created_at FROM users WHERE id=$1`, id,

    ).Scan(&u.ID, &u.Username, &u.Email, &u.Status, &u.CreatedAt)

    

    if errors.Is(err, pgx.ErrNoRows) {

        return nil, nil // 业务层自行判断

    }

    if err != nil {

        return nil, fmt.Errorf("pgx get user by id %d: %w", id, err)

    }

    return &u, nil

}

func (r *pgxUserRepo) Create(ctx context.Context, u *entity.User) error {

    _, err := r.pool.Exec(ctx,

        `INSERT INTO users(username, email, status, created_at) VALUES($1,$2,$3,NOW())`,

        u.Username, u.Email, u.Status,

    )

    if err != nil {

        return fmt.Errorf("pgx create user: %w", err)

    }

    return nil

}

3.3 Service:业务逻辑 + 事务控制

Go 复制代码
// service/user_service.go

package service

import (

    "context"

    "errors"

    "fmt"

    "yourproject/entity"

)

var ErrUserAlreadyExists = errors.New("user already exists")

type UserService struct {

    repo UserRepository

}

func NewUserService(repo UserRepository) *UserService {

    return &UserService{repo: repo}

}

func (s *UserService) CreateUser(ctx context.Context, u *entity.User) error {

    // 1. 实体级校验

    if err := u.Validate(); err != nil {

        return err

    }

    // 2. 业务规则:检查重名/重邮箱

    existing, err := s.repo.GetByEmail(ctx, u.Email)

    if err != nil {

        return fmt.Errorf("check email exists: %w", err)

    }

    if existing != nil {

        return ErrUserAlreadyExists

    }

    // 3. 持久化(事务由外层或 Service 统一控制)

    u.Status = 1 // 默认激活

    return s.repo.Create(ctx, u)

}

3.4 Handler:HTTP 适配 + DTO 转换

Go 复制代码
// handler/user_handler.go

package handler

import (

    "context"

    "errors"

    "net/http"

    "time"

    "github.com/gin-gonic/gin"

    "yourproject/entity"

    "yourproject/service"

)

// DTO:仅用于协议交互,不污染 Entity

type CreateUserRequest struct {

    Username string `json:"username" binding:"required,max=50"`

    Email    string `json:"email" binding:"required,email"`

}

type UserHandler struct {

    svc *service.UserService

}

func NewUserHandler(svc *service.UserService) *UserHandler {

    return &UserHandler{svc: svc}

}

func (h *UserHandler) Create(c *gin.Context) {

    var req CreateUserRequest

    if err := c.ShouldBindJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})

        return

    }

    // DTO → Entity

    user := &entity.User{

        Username: req.Username,

        Email:    req.Email,

    }

    // 调用 Service(继承请求上下文,支持超时/取消)

    if err := h.svc.CreateUser(c.Request.Context(), user); err != nil {

        switch {

        case errors.Is(err, service.ErrUserAlreadyExists):

            c.JSON(http.StatusConflict, gin.H{"error": "email already used"})

        default:

            c.JSON(http.StatusInternalServerError, gin.H{"error": "system error"})

        }

        return

    }

    c.JSON(http.StatusCreated, gin.H{"message": "created", "id": user.ID})

}

3.5 组装入口(main.go 精简版)

Go 复制代码
func main() {

    pool, _ := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))

    defer pool.Close()

    userRepo := postgres.NewUserRepo(pool)

    userSvc := service.NewUserService(userRepo)

    userHandler := handler.NewUserHandler(userSvc)

    r := gin.Default()

    r.POST("/api/users", userHandler.Create)

    r.Run(":8080")

}

四、生产级注意事项(90%团队踩过的坑)

坑 错误做法 正确姿势
🔁 Entity 与 DTO 混用 User 结构体直接加 binding:"required" 暴露给前端 严格分离:DTO 处理校验/序列化,Entity 承载业务规则
🌪️ 接口定义在实现层 infra/postgres/user_repo.go 里定义 interface 接口放 service/ 或 domain/,实现放 infra/(依赖倒置)
💧 事务边界混乱 Handler 里 db.Begin(),或 Repo 自己控制事务 事务永远在 Service 层 ,通过 tx 参数或统一包装函数传递
🕳️ context 断链 某一层用 context.Background() 重启上下文 所有方法首参必为 ctx,全程透传,支持超时/链路追踪/取消
📉 错误处理一刀切 全部返回 500 Internal Server Error 分层包装:Repo 包装系统错误,Service 抛业务错误,Handler 映射 HTTP 状态码
📦 为简单 CRUD 强上四层 查个字典表也建 Entity/Service/Repo/Handler 架构随复杂度演进 。简单查询可直接 Handler → Repo,复杂逻辑再抽 Service
🧪 无法单元测试 直接连数据库、依赖 HTTP 框架 面向接口编程。Service 测 mock Repo,Handler 测 mock Service,Repo 用 testcontainers

🔍 关键实践技巧

  1. DTO ↔ Entity 转换 :写独立 mapper 函数,不要用 copier 库隐藏逻辑。显式转换更安全、易调试。

  2. 错误分类 :

    Go 复制代码
    // service/errors.go
    
    var (
    
        ErrUserNotFound = errors.New("user not found")
    
        ErrInvalidEmail = errors.New("invalid email format")
    
    )

    Handler 用 errors.Is() 判断业务错误,其余统一 500 + 记录详细日志。

  3. 分页/排序查询 :不要在 Entity 里塞 Offset/Limit。用独立 QueryParams 结构体传入 Service,Repo 层动态拼接或使用查询构建器(如 squirrel)。

  4. 跨服务调用 :如果 Service 需要调用其他微服务,不要直接 import 对方 Handler/Repo 。定义 Client 接口,通过 HTTP/gRPC 客户端实现,保持层级纯洁。


五、何时用?何时不用?

场景 推荐架构 原因
MVP / 原型验证 Handler → Repo(扁平) 快速迭代,架构成本 > 业务收益
中大型业务系统 Handler + Service + Repo + Entity 边界清晰、可测试、易协作、支持多数据源
高频 CRUD 后台 Handler + Repo(Service 可省略) 业务逻辑极薄,强分层反增复杂度
云原生 / 微服务 Handler + Service + Repo + Entity + gRPC Client 独立部署、契约驱动、需强隔离
数据同步 / ETL 脚本 无分层(单文件/包) 生命周期短,跑完即弃

📈 演进路径建议 :
扁平结构 → 抽 Service 封装业务 → 定义 Repo 接口隔离存储 → 拆分 Domain/Infra 目录
不要一开始就画完美架构图。让架构随业务痛点自然生长。


六、架构师总结:四层架构的 4 条铁律

  1. 边界大于形式:四层不是目录命名游戏,而是职责隔离。每层只做一件事,做好一件事。
  2. 依赖单向流动 :Handler → Service → Repo Interface ← Repo Impl。打破单向,系统必然腐烂。
  3. Entity 是系统的锚:所有数据转换、校验、业务规则围绕 Entity 展开。它不该知道 HTTP 长什么样,也不该知道 SQL 怎么拼。
  4. 可测试是架构的试金石:如果某层无法脱离数据库/网络独立测试,说明依赖边界已经模糊。

🎯 最后一句真心话:
好的架构是"透明"的。业务开发时无感,出问题时秒级定位,重构时平滑替换。四层架构不是枷锁,而是让代码随业务自由生长的土壤。


📚 延伸资源(开发者直达)


💬 架构没有标准答案,只有与团队阶段匹配的权衡。

如果你正在重构老项目、设计新服务边界或制定团队规范,欢迎在评论区贴出你的目录结构或依赖痛点,我会给出针对性建议。

点赞 + 收藏,下次写业务代码时,直接抄作业 🏗️🚀

相关推荐
catchadmin5 分钟前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
开发语言·php
念何架构之路35 分钟前
go-grpc设计思想
开发语言·后端·golang
酷炫码神1 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
microrain1 小时前
SagooIoT 的缓存换后端只改一行配置,文件后端连 Redis 都不需要
物联网·缓存·golang·sagooiot
Elastic 中国社区官方博客2 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1882 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn3 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
X54先生(人文科技)3 小时前
社会嵌入式终身学习机器与修复时代-ELR架构和技术演化探讨
人工智能·深度学习·学习·架构·开源协议
喵了几个咪3 小时前
GoWind ShortPlay 深度拆解:三服务 BFF + Core,一条请求的完整工程链路
golang·gowind
传奇开心果编程3 小时前
【Rust入门练中学】第5课:引用与借用
开发语言·rust·引用·编程语言理论·通用编程语言·借用