1 目录概览

给代码安个家:后端分层艺术入门

🎭 我们的“建筑规划图”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
your project/
├── internal/         # 内部代码,Go 语言机制限制此目录只能被该项目内部引用
│   ├── domain/       # 领域层:业务核心,“原料”的定义与规则
│   ├── repository/   # 仓库层:数据的“仓库管理员”
│   │   ├── dao/      # 数据访问层:与数据库打交道的“搬运工”
│   │   └── cache/    # 缓存层:(可选)仓库里的“速取窗口”
│   ├── service/      # 服务层:业务逻辑的“总厨”
│   └── web/          # 表现层:与前端打交道的“服务员”
├── pkg/              # 公共代码,可以被外部项目引用的工具包
└── main.go           # 程序入口:“饭店老板”和“大厅入口”

👆 project/main.go(一个简单的“反面教材”)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

我们在 main.go 里接口写得很开心,但当业务变得复杂时,如果我们把数据库操作、业务逻辑、HTTP响应全都塞在一个文件里,很快就会变成一锅无法维护的“意大利面条”。

为了避免这种灾难,我们需要给代码做“功能分区”,让专业的人(代码块)做专业的事。

我们将借鉴一个高大上的概念—— DDD(领域驱动设计) 中的分层思想,引入 web - Service - Repository - DAO 四层结构(domain 层贯穿始终)。

先别被 DDD 吓到! 它本身博大精深,我们今天不是来研究它的哲学,而是像个小偷一样,悄悄“偷”走它几个最实用、最能提升代码整洁度的模式。

为了让你彻底理解这套结构,我们把项目服务想象成一家饭店的后厨


2 后厨角色分工 (各层职责)

web (Handler) - “服务员”

  • 职责:只负责和客人(HTTP 请求)打交道。他负责点菜、传菜,确保菜单(请求参数)没写错。
  • 原则:服务员不应该跑进厨房亲自炒菜。他只需要把菜单递给“总厨”就行了。所以,web 层只做参数校验、格式转换(如 JSON 转换)、调用 service 并返回响应。

service - “总厨”

  • 职责:后厨的灵魂人物,负责一道菜(一个完整的业务流程)的烹饪。
  • 工作方式:总厨看着菜单(业务需求),指挥手下的人去干活。他会对“仓库管理员”说:“去,给我拿条鱼和两个土豆来!” 拿到食材后,他会进行“烹饪”(处理业务逻辑),比如是清蒸还是红烧。
  • 核心价值:编排。service 负责协调一个或多个 repository 来共同完成一个复杂的业务动作(比如“用户注册”可能需要同时操作“用户表”和“积分表”)。

repository - “仓库管理员”

  • 职责:负责食材(数据)的取用和存储。总厨向他要东西,他保证能拿出来,service 层面向 repository 编程,而不是面向 dao。
  • 工作方式:总厨(service)只管要“一条鱼”(domain.User),但这条鱼到底是从 速取冰柜(cache 里拿的,还是从 冷库(database 里现捞的,总厨不关心。仓库管理员(repository 自己有一套工作流程,比如:
    1. 先去速取冰柜 (cache) 找。
    2. 找不到?再去冷库 (dao) 拿。
    3. 从冷库找到了,顺手在速取冰柜里也放一份,方便下次快取。
  • 核心价值repositoryservice 屏蔽了数据来源的细节。未来就算我们饭店升级,把冷库从 MySQL 换成了更高级的 MongoDB,也只需要换掉仓库管理员的具体操作方法 (dao),总厨的工作流程完全不受影响。另外 repository 还负责 domain 对象和 dao 对象的转换。

dao - “冷库搬运工”

  • 职责:只负责和数据库(冷库)这一个地方打交道。
  • 工作方式:他是最底层的执行者,懂得各种 SQL “咒语”(或 GORM 等 ORM 操作),负责把数据真真切切地从数据库的某个表里捞出来(得到 dao.User),或者存进去。

domain - “食材/菜品本身”

  • 职责:定义我们业务里最核心的东西是什么。在用户服务里,那自然就是“用户 (User)”这个概念。
  • 核心domain 不仅是数据结构(如 User 结构体),更重要的是它包含了业务规则和行为(如 user.ChangePassword() 这个方法,里面封装了“新密码不能为空”、“新旧密码不能相同”等业务逻辑)。

main.go - “饭店老板” (组装与启动)

  • 职责:饭店的创始人。他不负责日常运营(不写业务逻辑),他只在开业时做一件事:组装团队(依赖注入)。
  • 工作方式
  1. 老板先建好了“冷库”(初始化数据库连接)。
  2. 然后雇佣了 dao 搬运工(创建 DAO 实例),并告诉他冷库在哪。
  3. 接着雇佣了 repository 仓库管理员(创建 Repository 实例),并把 dao 和 cache 介绍给他。
  4. 再接着雇佣了 service 总厨(创建 Service 实例),并把 repository 介绍给他。
  5. 最后雇佣了 web 服务员(创建 Handler 实例),并把 service 介绍给他。
  6. 最后,老板把服务员(Handler)安排到大厅(gin.Engine 注册路由),饭店正式开业(r.Run())。

3 后厨工作流 (调用流程)

开业准备 (依赖注入)

如上所述,main.go 作为“老板”,在程序启动时,会自下而上地创建所有实例,并把下层的实例“注入”给上层。

初始化数据库NewDAONewRepository(dao)NewService(repo)NewHandler(svc)注册路由(handler)

日常运营 (请求处理)

后厨的指挥链条是单向的、自上而下的,绝对不允许“以下犯上”!

  1. 点菜 (Request): 服务员 (web) 接到菜单(HTTP 请求),校验后,交给 总厨 (service)
  2. 备料 (Process): 总厨 (service) 看完菜单,指挥 仓库管理员 (repository) 去拿“食材”(domain 对象)。
  3. 取货 (Storage): 仓库管理员 (repository)(可能先查 cache)指挥 搬运工 (dao) 去冷库里取货(dao 对象),拿到后转换成“食材”(domain 对象),再交给总厨。
    • 绝对禁止:搬运工 (dao) 绕过所有人直接把菜端给服务员 (web),或者服务员 (web) 直接冲进冷库 (dao) 拿东西。这就是跨层调用,是架构腐化的开始!
    • 出菜 (Response) 的流程则正好相反,是数据(或错误)一层层地往上传递: daorepositoryserviceweb

4 关键问题:Domain 和 DAO 的模型一样吗?

不一样!这是新手最容易混淆的地方。

  • domain.User(领域模型 DO):是“食材”。它代表了业务含义上的“用户”,只包含业务逻辑需要的数据和方法。它是纯净的,不应该包含 json:"name"gorm:"column:user_name" 这样的“标签”。
  • dao.User(数据模型 PO):是“冷库里的货品”。它代表了数据在数据库中是如何存储的。它包含 gormsqlx 等数据库操作标签,它的字段名可能和数据库表列名完全一致(比如 user_name)。

repository(仓库管理员)的核心职责之一,就是在这两者之间进行转换。

  • dao 从数据库查出 dao.User (PO)。
  • repositorydao.User 转换为 domain.User (DO),再返回给 service
  • service 处理完 domain.User (DO),交给 repository
  • repositorydomain.User (DO) 转换为 dao.User (PO),再交给 dao 存入数据库。

5 总结一下

  • 为什么要有 repositorydao 两层? repository 是个“抽象派”,它承诺“我会把业务对象(domain)搞定”,但不管具体怎么搞,它封装了缓存逻辑、DO/PO 转换,service 依赖它。

    • dao 是个“实干派”,专门负责用 MySQL (或者别的) 搞定“存储对象(dao)”。
    • 这样分工,以后换数据库(MySQL 换 MongoDB),只需要换掉 dao 的实现,repositoryservice 完全不用改。
  • service 层到底在忙啥?

    • 它就是那个发号施令的总厨。它的日常就是:问问这个 repository 拿到 domain.User,问问那个 repository 拿到 domain.Order,然后把这些“领域对象 (domain object)”组合起来,“卡卡卡”一顿操作(这就是核心业务逻辑!),最后做成一道精美的菜品,返回给 web

通过这样的分层,每一层的职责都非常清晰。我们的代码就像一个管理有序、效率极高的现代化厨房,而不是一个手忙脚乱、到处油污的苍蝇小馆。