我写了一个 Go 框架:用 DSL 替代 ORM,代码体积减半,开发效率翻倍

我写了一个 Go 框架:用 DSL 替代 ORM,代码体积减半,开发效率翻倍

这不是又一个"轮子"。这是我从 2024 年起,基于 7 年 Node 后端血泪经验,亲手从零打磨的元数据驱动 Go 后端框架。它用一套极简 DSL 替代了传统 ORM 和手写 SQL,让 85% 的业务场景只需声明"查什么表、选什么字段",框架自动完成参数校验、字段过滤、联表拆分、软删除、事务处理。


一、起因:我受够了什么?

做后端这些年,我反复踩过这些坑:

  1. ORM 的黑箱魔法 ------ GORM 的 Preload 一条语句生成 5 条 SQL,N+1 问题防不胜防;字段多了,Updates 误把零值当"不更新"。
  2. 手写 SQL 的低效 ------ 每个接口都要写 SELECT/INSERT/UPDATE,字段一改,所有 SQL 都得跟着改。
  3. 参数校验的重复劳动 ------ 每个接口都要写"这个字段必填、那个字段最长 50 字符",前端校验一套、后端校验又一套。
  4. 联表查询的痛苦 ------ JOIN 写多了性能差,拆分查询写起来又麻烦,到底该 JOIN 还是该拆?每次都要纠结。
  5. 多数据库兼容的噩梦 ------ MySQL 用 ? 占位符和反引号,PostgreSQL 用 $1 和双引号,AUTO_INCREMENT vs SERIAL......切换数据库基本等于重写一遍。

这些问题的共同根因是什么? 是"元数据"的缺失------表结构、字段属性、校验规则这些信息散落在代码各处以硬编码形式存在,没有被统一管理、统一消费。

于是,我开始思考:如果框架能读懂你的表结构,能不能自动帮你做这些事?


二、核心思想:元数据驱动 + 约定大于配置

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 框架架构感兴趣,欢迎点赞、收藏、评论交流。你的反馈是我持续打磨这个框架的动力。

相关推荐
明月_清风6 小时前
Go语言空接口与类型断言完全指南:从"万能容器"到"类型还原"
后端·go
蓝宝石的傻话9 小时前
security-collector-exporter:用Prometheus 解决 Linux 的安全审计
go
tyung10 小时前
Go 手写二叉堆优先队列:避开 container/heap 的性能陷阱
数据结构·后端·go
审判长烧鸡1 天前
【PHPer转Go】fmt vs log/slog
go·php
漓漾li1 天前
每日面试题(2026-05-20)- GO AI agent全栈
后端·架构·go
.魚肉1 天前
Raft 共识算法 · 演示系统(多终端)
算法·go·raft·分布式系统
审判长烧鸡1 天前
【Go工具】go-playground除了validator还有哪些常用的库
go·web
审判长烧鸡1 天前
Go 新版核心知识点合集(适配 Go1.18+ 含泛型 + 断言 + 接口 + 指针接收者全套)
go
审判长烧鸡2 天前
【Go工具】Go 标准库 VS go-playground
go