📌 写在前面:
很多 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 |
🔍 关键实践技巧
-
DTO ↔ Entity 转换 :写独立
mapper函数,不要用copier库隐藏逻辑。显式转换更安全、易调试。 -
错误分类 :
Go// service/errors.go var ( ErrUserNotFound = errors.New("user not found") ErrInvalidEmail = errors.New("invalid email format") )Handler 用
errors.Is()判断业务错误,其余统一500+ 记录详细日志。 -
分页/排序查询 :不要在 Entity 里塞
Offset/Limit。用独立QueryParams结构体传入 Service,Repo 层动态拼接或使用查询构建器(如squirrel)。 -
跨服务调用 :如果 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 条铁律
- 边界大于形式:四层不是目录命名游戏,而是职责隔离。每层只做一件事,做好一件事。
- 依赖单向流动 :
Handler → Service → Repo Interface ← Repo Impl。打破单向,系统必然腐烂。 - Entity 是系统的锚:所有数据转换、校验、业务规则围绕 Entity 展开。它不该知道 HTTP 长什么样,也不该知道 SQL 怎么拼。
- 可测试是架构的试金石:如果某层无法脱离数据库/网络独立测试,说明依赖边界已经模糊。
🎯 最后一句真心话:
好的架构是"透明"的。业务开发时无感,出问题时秒级定位,重构时平滑替换。四层架构不是枷锁,而是让代码随业务自由生长的土壤。
📚 延伸资源(开发者直达)
- 📖 Clean Architecture in Go - 经典分层模板
- 🛠️
sqlc+ 四层架构:类型安全代码生成实战 - 🧪 集成测试:Testcontainers-Go - 不依赖本地 DB 的测试方案
- 📐 依赖检查:
golangci-lint+depguard配置,CI 自动拦截违规引用
💬 架构没有标准答案,只有与团队阶段匹配的权衡。
如果你正在重构老项目、设计新服务边界或制定团队规范,欢迎在评论区贴出你的目录结构或依赖痛点,我会给出针对性建议。
点赞 + 收藏,下次写业务代码时,直接抄作业 🏗️🚀