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. 可测试是架构的试金石:如果某层无法脱离数据库/网络独立测试,说明依赖边界已经模糊。

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


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


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

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

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

相关推荐
skilllite作者6 小时前
Warp 终端效能与交互体验全景展示
人工智能·后端·架构·rust
Yang-Never6 小时前
Git -> Git Worktree 工作树
android·开发语言·git·android studio
riNt PTIP6 小时前
GO 快速升级Go版本
开发语言·redis·golang
xingpanvip6 小时前
星盘接口开发文档:日运语料接口指南
android·开发语言·前端·css·php·lua
AI进化营-智能译站6 小时前
ROS2 C++开发系列01:在ROS2上编写第一个C++ hello word
开发语言·c++·ai·word
我才是一卓6 小时前
2026 Python 入门教程,结合 vscode 和 miniforge/miniconda
开发语言·vscode·python
代码中介商6 小时前
Linux多线程编程完全指南(续):条件变量、读写锁与线程安全函数
linux·开发语言
其实防守也摸鱼7 小时前
CTF密码学综合教学指南--第二章
开发语言·网络·python·安全·网络安全·密码学·ctf
jimy17 小时前
C 语言的 static 关键字作用
c语言·开发语言·算法