应使用窄接口替代通用datalayer,按业务实体切分为userrepo、orderrepo等,各只暴露必要方法;sql外置到独立文件并用text/template解析;驱动注册需显式契约接口;事务必须由调用方显式传递,不得在repo内启动。

用窄接口替代通用 DataLayer
别一上来就定义一个包罗万象的 DataLayer 接口,把 Query、Exec、BeginTx 全塞进去。这种设计导致 mock 成本高、职责模糊、测试时不得不伪造整套连接逻辑。真正的窄接口是按业务实体和操作语义来切分的:UserRepo 只暴露 GetByID(ctx, id) 和 Create(ctx, u),OrderRepo 只管 ListByUserID(ctx, uid) 和 UpdateStatus(ctx, id, status)。每个方法内部用 *sql.DB 或 *sql.Tx 执行 SQL,不暴露底层连接细节,也不承担事务控制责任——那是调用方的事。
SQL 外置 + text/template 占位,别拼接字符串
硬编码 SQL 字符串会让 IDE 失去语法高亮、无法静态检查字段名、难以做 SQL 审计。把 SQL 拆到独立文件里,比如 sql/user_get_by_id.sql,内容写成:
SELECT id, name, email FROM users WHERE id = {{.ID}}
然后在代码里读取并执行:
- 用 text/template 解析,传入结构体(如 map[string]interface{}{"ID": 123})生成最终 SQL
- 不要用 fmt.Sprintf 或字符串拼接动态表名/字段名,那会引入 SQL 注入风险
- 模板只处理参数占位,不处理逻辑分支(如 {{if .WithDeleted}}),复杂条件交给 Go 层组装 SQL 片段
驱动注册必须显式,别用 interface{}
看到有人把 MySQL 驱动实例丢进 interface{} 再传给工厂函数?这等于放弃编译期类型检查。一旦调用 Query 方法,运行时 panic 的概率极高,而且你根本没法静态知道这个驱动支不支持批量插入或 Savepoint。正确做法是定义契约接口:
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- DBExecutor 接口只包含 Query(query string, args ...any) (Rows, error) 和 Exec(query string, args ...any) (Result, error)
- 每个驱动(mysql.Factory、postgres.Factory)实现 DriverFactory,返回封装好的 DBExecutor
- DSN 解析统一走 url.Parse,取 u.Scheme 去查注册表:factories["mysql"] 或 factories["pg"]
- 未注册协议时直接返回明确错误,比如 unsupported driver scheme: sqlite3,不静默 fallback
事务必须显式传递,别藏在 Repo 里
常见错误是让 UserRepo 自己调用 db.Begin(),结果事务边界失控、嵌套事务行为不可预测、测试时难以注入 mock 事务。所有涉及事务的操作,必须由上层决定是否开启,并显式传入 *sql.Tx:
- UserRepo 提供两个版本方法:GetByID(ctx, id)(用 *sql.DB)和 GetByIDTx(ctx, tx, id)(用 *sql.Tx)
- 调用方在 handler 或 service 层统一管理 tx := db.Begin() 和 tx.Commit()/tx.Rollback()
- 如果某个业务需要跨多个 Repo 操作(如创建用户+初始化配置),它们都得接收同一个 *sql.Tx 实例,而不是各自开事务
真正难的不是定义接口,而是守住边界:SQL 不该出现在业务逻辑里,事务不该由数据访问层启动,驱动能力不该靠运行时断言判断。每条线划清楚了,抽象才不会变成新包袱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










