我写了一个 Go 框架:用 DSL 替代 ORM,代码体积减半,开发效率翻倍
这不是又一个"轮子"。这是我从 2024 年起,基于 7 年 Node 后端血泪经验,亲手从零打磨的元数据驱动 Go 后端框架。它用一套极简 DSL 替代了传统 ORM 和手写 SQL,让 85% 的业务场景只需声明"查什么表、选什么字段",框架自动完成参数校验、字段过滤、联表拆分、软删除、事务处理。
一、起因:我受够了什么?
做后端这些年,我反复踩过这些坑:
- ORM 的黑箱魔法 ------ GORM 的
Preload一条语句生成 5 条 SQL,N+1 问题防不胜防;字段多了,Updates误把零值当"不更新"。 - 手写 SQL 的低效 ------ 每个接口都要写 SELECT/INSERT/UPDATE,字段一改,所有 SQL 都得跟着改。
- 参数校验的重复劳动 ------ 每个接口都要写"这个字段必填、那个字段最长 50 字符",前端校验一套、后端校验又一套。
- 联表查询的痛苦 ------ JOIN 写多了性能差,拆分查询写起来又麻烦,到底该 JOIN 还是该拆?每次都要纠结。
- 多数据库兼容的噩梦 ------ MySQL 用
?占位符和反引号,PostgreSQL 用$1和双引号,AUTO_INCREMENTvsSERIAL......切换数据库基本等于重写一遍。
这些问题的共同根因是什么? 是"元数据"的缺失------表结构、字段属性、校验规则这些信息散落在代码各处以硬编码形式存在,没有被统一管理、统一消费。
于是,我开始思考:如果框架能读懂你的表结构,能不能自动帮你做这些事?
二、核心思想:元数据驱动 + 约定大于配置
2.1 一切从 DesignMetaData 出发
在我的框架里,你只需要在一个地方定义表结构:
go
// app/design/tables.go
"user": {
Cname: "用户",
Fields: baseDesign.GetStandardFieldsInfos([]interface{}{
"T_Phone", // 手机号(内置校验规则:格式、长度)
"T_UserName|name", // 字段模板 | 字段名
"T_Gender", // 性别
"T_Amount|coin|金币余额", // 字段模板 | 字段名 | 注释
"T_AccountStatus", // 账号状态
}, nil),
}
这一份声明,框架会自动推导出:
- 建表 DDL(含字段类型、默认值、注释)
- 参数校验规则(类型、必填、长度、格式)
- API 文档(Swagger 注解)
- 字段权限(哪些字段可读、可写)
- 索引策略(唯一索引、普通索引)
传统开发中,建表、写模型、写校验、写文档是四份独立的工作 。现在,一份声明,四处消费。
2.2 字段模板:消灭重复字段定义
你有没有遇到过这种情况:10 张表都有 phone 字段,每张表都要写一遍 VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号'?
我的解决方案是 字段模板(T_ 前缀):
go
// 框架内置模板(可扩展)
"T_Phone" → type: VARCHAR(20), 校验: 手机号正则, 脱敏: 138****1234
"T_UserName" → type: VARCHAR(50), 校验: 4-20字符, 脱敏: 无
"T_PasswordHash"→ type: VARCHAR(255), 校验: 无, 脱敏: 无(bcrypt不可逆)
"T_Gender" → type: SMALLINT, 校验: 0/1/2枚举, 脱敏: 无
声明 "T_Phone" 一行,框架自动补齐完整的字段定义、校验规则和脱敏策略。
三、杀手锏:极简 DSL
这是框架最核心的创新点。我设计了一套类自然语言的 DSL 语法,让85% 的 SQL 编写工作变成字符串声明。
3.1 表与字段 DSL
go
// 单表 - 查所有字段
tableOrTables := "user"
// 单表 - 指定字段
tableOrTables := "user|id,name,gender"
// 单表 - 带别名
tableOrTables := "user:u|id,name,gender"
// 单表 - 排除字段(! 前缀)
tableOrTables := "user|!password,is_on"
// 单表 - 字段取别名
tableOrTables := "user|id,name:user_name,avatar:avatar_url"
// 单表 - 聚合表达式
tableOrTables := "user|id,COUNT(id):order_count,SUM(coin):total_coin"
// 单表 - 窗口函数
tableOrTables := "user|id,name,RANK() OVER (ORDER BY coin DESC):rank"
3.2 多表 JOIN DSL(数组声明)
go
// LEFT JOIN(自动推断关联字段 order.user_id = user.id)
tableOrTables := MergeTables{
"user:u|id,name,gender,email",
"<-",
"order:o|id,order_no,pay_status",
}
// LEFT JOIN(显式指定关联字段)
tableOrTables := MergeTables{
"user:u|id,name,email",
"user_id <- id",
"order:o|id,order_no,pay_type",
}
// INNER JOIN
tableOrTables := MergeTables{
"user:u|id,name",
"-",
"order:o|id,order_no",
}
// RIGHT JOIN / FULL JOIN
// 同理:-> 表示 RIGHT JOIN,<-> 表示 FULL JOIN
3.3 WHERE 条件 DSL
go
// 等值比较
where := map[string]interface{}{"gender": 1} // gender = 1
where := map[string]interface{}{"gender|!": 1} // gender != 1
where := map[string]interface{}{"age|>": 18} // age > 18
where := map[string]interface{}{"age|>=": 18} // age >= 18
// IN / NOT IN
where := map[string]interface{}{"user_id": []int{1, 2, 3}} // user_id IN (1,2,3)
where := map[string]interface{}{"user_id|!": []int{1, 2, 3}} // user_id NOT IN (1,2,3)
// BETWEEN(自动识别数组长度为2的值)
where := map[string]interface{}{"create_time": [2]string{"2025-06-01", "2025-06-30"}}
// LIKE(% 的位置决定匹配方式)
where := map[string]interface{}{"name|%like%": "四"} // LIKE '%四%'
where := map[string]interface{}{"name|like%": "王"} // LIKE '王%'
where := map[string]interface{}{"name|%like": "梅"} // LIKE '%梅'
where := map[string]interface{}{"name|___": "刘乐梅"} // LIKE '___'(定长)
// NULL 判断
where := map[string]interface{}{"refund_time": nil} // IS NULL
where := map[string]interface{}{"refund_time|!": nil} // IS NOT NULL
// 正则匹配(/ 包裹正则)
where := map[string]interface{}{"phone|/^184[0-9]*$/": true} // REGEXP
// 字段间比较(FIELD 前缀)
where := map[string]interface{}{"update_time|FIELD>": "create_time"} // update_time > create_time
// OR 条件组
where := map[string]interface{}{
"getConds": []map[string]string{
{"name|%LIKE%": "四"},
{"phone|%LIKE%": "四"},
},
} // (name LIKE '%四%' OR phone LIKE '%四%')
3.4 排序、分组、分页 DSL
go
queryParams := S_QueryParams{
// 排序
OrderBy: []S_OrderByItem{
{Field: "gender", Type: "ASC"},
{Field: "coin", Type: "DESC"},
},
// 分组
GroupBy: []string{"user_id"},
// 分组后过滤
Having: map[string]interface{}{"SUM(pay_amount)|>": 200},
// 分页
Limits: [2]int{1, 10}, // page=1, size=10
}
3.5 一个完整的查询长什么样?
go
// 查询:购买了会员的男性用户订单列表(按时间倒序,第1页10条)
mergeTables := MergeTables{
"user:u|id,name,gender",
"<-",
"order:o|id,order_no,create_time,pay_status,pay_amount",
}
queryParams := S_QueryParams{
Where: map[string]interface{}{
"gender": 1, // 男性
"pay_type": 1, // 会员购买
},
OrderBy: []S_OrderByItem{
{Field: "o.create_time", Type: "DESC"},
},
Limits: [2]int{1, 10},
}
// 框架自动生成 SQL:
// SELECT u.id, u.name, u.gender, o.id, o.order_no, o.create_time, o.pay_status, o.pay_amount
// FROM "user" u
// LEFT JOIN "order" o ON o.user_id = u.id
// WHERE u.gender = 1 AND o.pay_type = 1 AND u.deleted_time IS NULL AND o.deleted_time IS NULL
// ORDER BY o.create_time DESC
// LIMIT 10 OFFSET 0
注意到了吗? deleted_time IS NULL 是框架自动注入的------你不需要每次写软删除条件。
四、框架架构:严格的五层分离
markdown
HTTP 请求
↓
Router(路由层) → 注册路由,绑定 Controller
↓
Controller(控制层)→ 声明 DSL + 校验规则,薄薄一层
↓
Service(服务层) → 业务逻辑编排
↓
Repository(仓储层)→ 数据访问,组装 SQL
↓
Database(数据库)
Controller 层的极致简洁:
go
// 查询用户列表 ------ 只需声明三样东西:表字段、校验规则、调用 Service
func SelectList(c *gin.Context) {
tableOrTables := "user|id,name,phone,email,avatar,status,create_time"
HandleResponse(c, nil, func(params ReqParams, standLeftTables StandLeftMergeTables) gin.H {
queryParams := baseController.GetSelectListQueryParams(params)
data, err := userService.SelectList(standLeftTables, queryParams)
if err != nil {
return gin.H{"msg": err}
}
return gin.H{"data": data}
}, tableOrTables)
}
一个标准的列表接口,Controller 层不到 10 行代码。没有参数解析、没有 SQL 拼接、没有手动校验。
五、插件化:可插拔的企业级能力
框架采用微内核 + 插件架构。核心只提供最基础能力,其余功能全部通过插件实现:
| 插件 | 功能 | 替换方案 |
|---|---|---|
db-mysql |
MySQL 方言 | db-postgresql 无缝切换 |
cache-redis |
Redis 缓存 | cache-memory(开发环境) |
auth-jwt |
JWT 认证 | auth-rbac(权限) |
upload-local |
本地上传 | upload-oss(阿里云 OSS) |
captcha-image |
图片验证码 | 可替换其他厂商 |
audit-log |
审计日志 | 按需启用 |
http-protection |
熔断器 | 保护系统稳定 |
swagger |
API 文档 | 自动生成 |
数据库方言切换只需改一行配置:
yaml
# config.yaml
database:
dialect: postgresql # 改为 mysql 即可切换
框架通过 schemaDialect 接口抽象了所有数据库差异------占位符、引号、自增语法、JSON 查询、LIMIT/OFFSET、UPSERT......上层业务代码零感知。
六、智能化:框架帮你做的那些"脏活累活"
6.1 自动参数校验
框架根据 DesignMetaData 中的字段定义,自动生成校验规则:
T_Phone→ 自动校验手机号格式T_Email→ 自动校验邮箱格式T_UserName→ 自动校验 4-20 字符NotNull标记 → 自动校验必填- 前端传了不存在的字段 → 自动忽略(防注入)
6.2 自动软删除过滤
所有查询自动注入 deleted_time IS NULL,你永远不用担心查出已删除的数据。
6.3 自动联表拆分查询
当 JOIN 的表超过 2 张时,框架会智能判断:是执行一次 JOIN 查询,还是拆分成多条单表查询再在内存中组装?这个决策基于数据量估算和索引分析。
6.4 自动脱敏
在 API 响应中,手机号自动脱敏为 138****1234,邮箱脱敏为 u***@example.com,身份证号脱敏为 330***********1234------所有这些规则都在字段模板中配置一次,全局生效。
七、数据说话:效率提升到底有多少?
以一个典型的中型后端项目(20 张表、100+ 接口)为例:
| 维度 | 传统方式(GORM + 手写) | QuickDriver | 提升 |
|---|---|---|---|
| 建表 DDL 编写 | 20 个 SQL 文件,逐个手写 | 1 个 Go 文件,声明表结构 | 减少 90% |
| 参数校验代码 | 每个接口 5-15 行校验逻辑 | 自动完成,0 行代码 | 减少 100% |
| CRUD 接口开发 | 平均 30-50 行/接口 | 平均 5-15 行/接口 | 减少 70% |
| SQL 拼接代码 | 2000+ 行分散在各处 | 集中 DSL 解析,约 500 行 | 减少 75% |
| 数据库切换成本 | 几乎不可行(重写所有 SQL) | 改一行配置 | 减少 99% |
| 字段修改影响面 | 需改 SQL、校验、文档 | 只改 DesignMetaData 一处 | 减少 80% |
| 软删除遗漏 Bug | 常见(忘记加 WHERE deleted IS NULL) | 不可能(框架自动注入) | 消除 100% |
八、技术挑战与思考
8.1 DSL 设计的平衡术
DSL 设计最大的挑战是在简洁性和表达能力之间找到平衡。太简单 → 复杂查询写不了;太复杂 → 又变成了另一种 SQL。
我的设计原则是:80% 场景用极简 DSL 覆盖,20% 复杂场景开放 RawSQL 出口。
go
// 极复杂查询:框架不拦你,但要求你写清楚原因
where := baseSql.RawCond(
"EXISTS (SELECT 1 FROM vip WHERE vip.user_id = user.id AND vip.level > ?)",
[]interface{}{3},
"查询 VIP3 级以上用户", // ← 必须写 Reason,方便 Code Review
)
8.2 元数据驱动的代价
元数据驱动的好处是"一次定义,处处消费",代价是启动时要解析所有 DesignMetaData。对于 20 张表的项目,解析耗时约 50ms,完全可接受。对于百张表的项目,框架内部有缓存机制避免重复解析。
8.3 方言接口的设计
go
type schemaDialect interface {
Placeholder(index int) string // MySQL: "?" PostgreSQL: "$1"
QuoteIdent(name string) string // MySQL: `name` PostgreSQL: "name"
AutoIncrementClause() string // MySQL: AUTO_INCREMENT PG: SERIAL
JSONExtract(col, path string) string // 不同数据库的 JSON 查询语法
// ... 更多方言差异
}
这个接口是框架多数据库兼容的核心。每接入一个新数据库,只需实现这个接口,所有上层 CRUD 代码无需修改。
九、项目现状与规划
已验证能力(覆盖 85% 业务场景):
- 用户系统(注册、登录、注销、验证码)
- 完整 CRUD(单表/多表/树形、批量操作、软/硬删除)
- 智能参数校验、接口权限、字段过滤
- 多数据库方言(PostgreSQL/MySQL)
- 插件化架构(缓存、上传、认证、审计、熔断)
- 55 个接口 + 176 个参数场景的测试覆盖
路线图:
- 🚧 导入/导出功能完善
- 🚧 智能连表拆分查询
- 🚧 分表支持(水平拆分/垂直拆分)
- 📋 全文搜索集成
- 📋 微服务拆分支持
十、写在最后
这个项目是我对"后端开发应该是什么样"的一次完整实践。
我不相信 ORM 是银弹,也不相信手写 SQL 是唯一出路。我相信的是:框架应该做更多"脏活累活",让开发者专注于业务逻辑。
用元数据去描述你的数据结构,用 DSL 去表达你的查询意图,把 SQL 优化、参数校验、字段过滤、软删除这些机械劳动全部交给框架------这是我理解的"极致开发体验"。
代码是写给人看的,只是恰好机器能够执行。
------《计算机程序的构造和解释》
项目地址: quick-driver(即将开源,敬请关注)
技术交流: 微信 fic3014 | 邮箱 1583187609@qq.com
如果你对元数据驱动、DSL 设计、Go 框架架构感兴趣,欢迎点赞、收藏、评论交流。你的反馈是我持续打磨这个框架的动力。