第三章 目录结构设计
文章目录
1 目录概览
给代码安个家:后端分层艺术入门
🎭 我们的“建筑规划图”
|
|
👆 project/main.go(一个简单的“反面教材”)
|
|
我们在 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) 自己有一套工作流程,比如:- 先去速取冰柜 (
cache) 找。 - 找不到?再去冷库 (
dao) 拿。 - 从冷库找到了,顺手在速取冰柜里也放一份,方便下次快取。
- 先去速取冰柜 (
- 核心价值:
repository对service屏蔽了数据来源的细节。未来就算我们饭店升级,把冷库从 MySQL 换成了更高级的 MongoDB,也只需要换掉仓库管理员的具体操作方法 (dao),总厨的工作流程完全不受影响。另外 repository 还负责 domain 对象和 dao 对象的转换。
dao - “冷库搬运工”
- 职责:只负责和数据库(冷库)这一个地方打交道。
- 工作方式:他是最底层的执行者,懂得各种 SQL “咒语”(或 GORM 等 ORM 操作),负责把数据真真切切地从数据库的某个表里捞出来(得到 dao.User),或者存进去。
domain - “食材/菜品本身”
- 职责:定义我们业务里最核心的东西是什么。在用户服务里,那自然就是“用户 (User)”这个概念。
- 核心:
domain不仅是数据结构(如 User 结构体),更重要的是它包含了业务规则和行为(如 user.ChangePassword() 这个方法,里面封装了“新密码不能为空”、“新旧密码不能相同”等业务逻辑)。
main.go - “饭店老板” (组装与启动)
- 职责:饭店的创始人。他不负责日常运营(不写业务逻辑),他只在开业时做一件事:组装团队(依赖注入)。
- 工作方式:
- 老板先建好了“冷库”(初始化数据库连接)。
- 然后雇佣了 dao 搬运工(创建 DAO 实例),并告诉他冷库在哪。
- 接着雇佣了 repository 仓库管理员(创建 Repository 实例),并把 dao 和 cache 介绍给他。
- 再接着雇佣了 service 总厨(创建 Service 实例),并把 repository 介绍给他。
- 最后雇佣了 web 服务员(创建 Handler 实例),并把 service 介绍给他。
- 最后,老板把服务员(Handler)安排到大厅(gin.Engine 注册路由),饭店正式开业(r.Run())。
3 后厨工作流 (调用流程)

开业准备 (依赖注入)
如上所述,main.go 作为“老板”,在程序启动时,会自下而上地创建所有实例,并把下层的实例“注入”给上层。
初始化数据库 → NewDAO → NewRepository(dao) → NewService(repo) → NewHandler(svc) → 注册路由(handler)
日常运营 (请求处理)
后厨的指挥链条是单向的、自上而下的,绝对不允许“以下犯上”!
- 点菜 (Request): 服务员 (
web) 接到菜单(HTTP 请求),校验后,交给 总厨 (service)。 - 备料 (Process): 总厨 (
service) 看完菜单,指挥 仓库管理员 (repository) 去拿“食材”(domain对象)。 - 取货 (Storage): 仓库管理员 (
repository)(可能先查cache)指挥 搬运工 (dao) 去冷库里取货(dao对象),拿到后转换成“食材”(domain对象),再交给总厨。 -
- 绝对禁止:搬运工 (
dao) 绕过所有人直接把菜端给服务员 (web),或者服务员 (web) 直接冲进冷库 (dao) 拿东西。这就是跨层调用,是架构腐化的开始!
- 绝对禁止:搬运工 (
-
- 出菜 (Response) 的流程则正好相反,是数据(或错误)一层层地往上传递:
dao→repository→service→web
- 出菜 (Response) 的流程则正好相反,是数据(或错误)一层层地往上传递:
4 关键问题:Domain 和 DAO 的模型一样吗?
不一样!这是新手最容易混淆的地方。
domain.User(领域模型 DO):是“食材”。它代表了业务含义上的“用户”,只包含业务逻辑需要的数据和方法。它是纯净的,不应该包含json:"name"或gorm:"column:user_name"这样的“标签”。dao.User(数据模型 PO):是“冷库里的货品”。它代表了数据在数据库中是如何存储的。它会包含gorm或sqlx等数据库操作标签,它的字段名可能和数据库表列名完全一致(比如user_name)。
repository(仓库管理员)的核心职责之一,就是在这两者之间进行转换。
dao从数据库查出dao.User(PO)。repository将dao.User转换为domain.User(DO),再返回给service。service处理完domain.User(DO),交给repository。repository将domain.User(DO) 转换为dao.User(PO),再交给dao存入数据库。
5 总结一下
-
为什么要有
repository和dao两层?repository是个“抽象派”,它承诺“我会把业务对象(domain)搞定”,但不管具体怎么搞,它封装了缓存逻辑、DO/PO 转换,service依赖它。dao是个“实干派”,专门负责用 MySQL (或者别的) 搞定“存储对象(dao)”。- 这样分工,以后换数据库(MySQL 换 MongoDB),只需要换掉
dao的实现,repository和service完全不用改。
-
service层到底在忙啥?- 它就是那个发号施令的总厨。它的日常就是:问问这个
repository拿到domain.User,问问那个repository拿到domain.Order,然后把这些“领域对象 (domain object)”组合起来,“卡卡卡”一顿操作(这就是核心业务逻辑!),最后做成一道精美的菜品,返回给web。
- 它就是那个发号施令的总厨。它的日常就是:问问这个
通过这样的分层,每一层的职责都非常清晰。我们的代码就像一个管理有序、效率极高的现代化厨房,而不是一个手忙脚乱、到处油污的苍蝇小馆。