项目结构
一个良好、一致的项目结构是软件工程的最佳实践之一。Maltose 通过 maltose new 命令为用户提供了一套经过精心设计的、推荐的项目结构,旨在实现关注点分离,提高项目的可维护性和可扩展性。
命名规范
在 Maltose 中,我们推荐遵循一套能最大化利用代码生成优势的命名规范。这能极大地提升项目的可读性和可维护性。
快速概览:一个完整的例子
我们以一个名为 user-center 的用户中心模块为例,看它在 Maltose 的工作流中如何演变:
| 阶段 | 示例 | 规范 (Case) | 说明 |
|---|---|---|---|
| 1. API 设计 | api/user-center/v1/ | Kebab-case | 目录名:使用连字符,对 URL 和 DevOps 生态友好。 |
| 2. Service 生成 | internal/service/user_center.go | Snake-case | 文件名:自动转换为下划线,符合 Go 社区习惯。 |
| 3. Logic 生成 | internal/logic/usercenter/ | lowerCase | 逻辑层包/目录:自动转换为小写单词,符合 Go 包规范。 |
| 4. Go 代码内部 | type sUserCenter struct {} | CamelCase | 类型名:自动转换为驼峰式,符合 Go 语法要求。 |
遵循这套规范,您可以在享受目录命名便利性的同时,产出完全符合 Go 社区规范的内部代码。
核心原则详解
目录名 (Modules): 使用连字符 (kebab-case)
- 这是 Web 和云原生生态的事实标准。Maltose 的生成工具会为您处理好后续的转换。
- 示例:
api/user-center/,api/order-management/
Go 文件名 (Filenames): 使用下划线 (snake_case)
- 这是 Go 社区的编码风格共识,可读性好。
- 示例:
user_center.go,order_management.go
Go 包名 (Packages): 使用小写单个单词
- 这是 Go 语言的官方约定。对于需要复合词的复杂包名,最佳实践是直接将小写单词连接起来(如
package httpclient),并绝对避免使用下划线或驼峰式作为包名。 - 示例:
package service,package usercenter
- 这是 Go 语言的官方约定。对于需要复合词的复杂包名,最佳实践是直接将小写单词连接起来(如
Go 类型名 (Types): 使用驼峰式 (CamelCase)
- 这是 Go 语言的强制语法要求,用于区分导出和非导出成员。
- 示例:
type sUserCenter struct {},type cOrderManagement struct {}
项目结构
当您执行 maltose new <project-name> 后,会得到如下的目录结构:
.
├── api/ # API 定义和请求/响应结构
│ └── v1/ # API 版本
├── cmd/ # 应用程序入口
│ ├── server.go # 服务启动
│ └── config.go # (可选) 配置加载及钩子(Hook)函数
├── config/ # 配置文件
├── internal/ # 内部代码,不对外暴露
│ ├── controller/ # 控制器
│ ├── dao/ # 数据访问对象
│ ├── logic/ # 业务逻辑实现
│ ├── model/ # 数据模型
│ │ ├── entity/ # 数据库实体
│ │ └── do/ # (可选) 业务数据对象
│ ├── provider/ # 第三方服务封装
│ └── service/ # 服务接口定义
├── utility/ # 通用工具
├── go.mod
└── main.go # 项目主入口以下是各主要目录和文件的职责说明:
/api
存放 API 的定义文件(.go 格式)。这里的"定义"主要指请求和响应的 struct,是项目的对外契约。
支持的路径格式
maltose gen service 命令能够智能地解析 api 目录下的文件路径,以确定生成的 Controller 和 Service 的模块名和版本。支持以下格式:
- 模块化版本 (推荐):
api/<module>/<version>/<file>.go(例如:api/user/v1/user.go) - 纯版本:
api/<version>/<file>.go(例如:api/v1/user.go) - 模块化:
api/<module>/<file>.go(例如:api/user/user.go) - 此时版本将默认为v1。
/cmd
项目的启动入口层。负责应用的启动、依赖注入、路由注册等核心初始化工作。
/config
存放应用的配置文件,例如 config.yaml。
/internal
存放项目内部的业务逻辑代码,这是项目的主要工作区。Go 语言的机制确保了 internal 包下的代码只能被项目内部引用,无法被外部项目导入。
- /controller: 控制器层。负责接收和解析来自路由的请求,进行参数校验,然后调用
Logic层处理业务,最后向客户端返回响应。 - /service: 服务接口层。定义了业务逻辑所需的接口 (
interface)。 - /logic: 业务逻辑层。这是实现
service接口的地方,它会组织和编排DAO和Provider提供的能力来完成一个完整的业务流程。 - /dao: 数据访问对象 (DAO) 层。封装了对数据库的底层操作,直接与数据库交互。
- /provider: 第三方服务封装层。当您的业务需要调用外部 API(例如,其他微服务、SaaS 服务如短信邮件等)时,建议在此目录下进行封装。
- /model: 数据模型层。存放项目所有的数据结构定义。
- /entity: 数据实体 (Entity)。存放与数据库表结构一一对应的
struct。 - /do: (可选) 数据对象 (Data Object)。手动创建,用于承载复杂的数据库查询结果。
- /entity: 数据实体 (Entity)。存放与数据库表结构一一对应的
关于 Provider 层的最佳实践
Provider 层是解耦内部业务逻辑与外部服务依赖的关键。一个设计良好的 Provider 层可以显著提高项目的可测试性和可维护性。
- 职责:
Provider的核心职责是将所有对外部服务的调用(例如,调用其他微服务、请求第三方 SaaS 平台 API)封装起来,对Logic层屏蔽其实现细节。 - 实现:
- 建议为每一个外部服务创建一个单独的
provider包,例如internal/provider/user_service。 - 在包内,可以定义一个
struct,并为其实现调用外部服务的具体方法。 - 内部应优先使用框架提供的
mclient来执行 HTTP/gRPC 请求。
- 建议为每一个外部服务创建一个单独的
- 使用:
Logic层通过依赖注入的方式直接使用具体的Provider实现,而无需在Service层为其定义接口。- 这种做法可以避免为不属于核心业务领域的外部调用创建过多的
interface,保持Service层的纯粹性,同时也简化了依赖关系。
关于 do 和 entity 的思考
为了保持框架的轻量级和灵活性,maltose 在数据模型层提供了 entity 和 do 两种结构,但它们的定位与传统重型框架有所不同。
entity(实体) - 基础数据模型- 职责:
entity的结构与数据库表结构 严格 1:1 对应。maltose gen model命令会为您自动生成并维护它。 - 定位: 它是所有标准、简单 CRUD 场景下的 默认且推荐 的数据模型。
gen dao命令生成的默认方法也直接使用entity。
- 职责:
do(数据对象) - 可选的高级扩展- 职责:
do目录默认不被强制使用,它是一个可选的、需要您手动创建的目录,用于解决entity无法优雅处理的复杂场景。 - 定位: 它不是一个被强制推行的规范,而是一个解决特定问题的"工具箱"。如果您的项目业务逻辑简单,完全可以忽略或删除
do目录。
- 职责:
什么时候应该手动创建和使用 do?
当您的业务发展到一定复杂度,遇到以下场景时,do 层将是您的得力助手:
承载复杂数据库查询结果: 当您需要执行多表
JOIN或其他复杂 SQL 查询时,其返回结果的结构往往与任何单一的entity都不匹配。此时,在internal/model/do/目录下手动创建一个struct来定义这个专用的结果集,是最佳实践。聚合内部业务数据: 当
Service层需要将来自不同数据源(例如,MySQL 中的entity、Redis 缓存、第三方 API 返回的数据)的信息组合成一个统一的对象,以便在业务逻辑内部流转时,do提供了一个理想的"聚合模型"存放地。
简而言之,maltose 的设计哲学是:entity 为本,do 为用。我们默认提供最轻量、最直接的 entity 方案满足 80% 的常规需求,同时保留了 do 这一扩展能力,让开发者在面对 20% 的复杂问题时,有路可循,有章法可依。
/utility
存放项目范围内的通用工具函数或模块,例如统一的日志记录器、错误码定义等。
通过遵循这套项目结构,您可以更轻松地管理代码,并与团队成员高效协作。