因为 database/sql 仅提供驱动统一接口,不处理连接池策略、事务传播、上下文透传等业务需求,且各驱动在事务类型、sql 方言、返回值语义上存在不可忽视的差异。

为什么不能直接用 database/sql 做抽象层
因为 database/sql 本身只是 SQL 驱动的统一接口,不处理连接池管理策略、事务传播、上下文超时透传、多数据源路由等业务层关心的问题。你写个 db.QueryRow 看似跨驱动,但一旦加了 pgx.TxOptions 或 mysql.Option 就立刻泄漏底层细节。
常见错误现象:单元测试里 mock *sql.DB 后,发现事务方法(如 BeginTx)返回类型是驱动私有 struct,无法被 interface 捕获;或者换用 SQLite 做本地测试时,INSERT ... RETURNING 直接 panic。
- 真正需要抽象的不是“执行 SQL”,而是“如何安全地开启/提交/回滚”“如何绑定上下文生命周期”“如何隔离读写连接”
- 不要试图把
Exec/Query方法塞进一个大 interface——它们的参数和返回值在不同驱动下语义不一致(比如pgx.Conn的Query返回pgx.Rows,而sql.DB返回*sql.Rows) - 优先封装行为,而非函数签名。例如定义
StoreUser(ctx context.Context, u User) error,而不是暴露ExecContext
用 interface{} + 组合实现轻量存储适配器
Go 没有泛型 interface 继承,但可以用组合+嵌入隐藏具体驱动。核心是把“连接获取”和“事务控制”拆成两个可替换的组件:
// Adapter 是运行时绑定的具体实现
type Adapter interface {
GetConn(ctx context.Context) (Conn, error)
BeginTx(ctx context.Context, opts TxOptions) (Tx, error)
}
type Conn interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
type Tx interface {
Conn
Commit() error
Rollback() error
}
这样 PostgreSQL 和 MySQL 实现可以各自 new 出 *pgx.Conn 或 *sql.Conn,再包装一层适配器,对外只暴露 Conn 和 Tx 接口。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 避免在 adapter 层做 SQL 转义或方言适配——那是 ORM 或查询构建器的事,存储抽象层只管“连上”和“执行”
-
GetConn必须支持 context timeout,否则连接泄漏无法感知;建议内部调用db.Conn(ctx)而非复用*sql.DB的Query方法 - 如果业务需要读写分离,
GetConn可接受RoleRead/RoleWrite参数,由具体 adapter 决定走哪个连接池
sqlmock 在抽象层测试中的局限性
sqlmock 只能 mock *sql.DB,对 pgxpool.Pool 或原生 pgx.Conn 完全无效。当你用 adapter 抽象后,mock 对象必须实现 Adapter 接口,而非 *sql.DB。
常见错误现象:测试里传了个 sqlmock.New() 进 service 层,结果调用 BeginTx 时 panic “interface conversion: *sqlmock.Sqlmock is not store.Tx”。
- 正确做法是手写一个内存实现:
memAdapter,用sync.Map存数据,BeginTx返回带 rollback 标志的 fakeTx - 不要 mock 所有方法——只 mock 你当前测试路径实际调用的那几个,比如测
StoreUser就只需实现BeginTx→ExecContext→Commit - 如果依赖具体驱动特性(如 pgx 的
CopyFrom),测试时应启动本地 Docker 实例跑真实 SQL,而不是强行 mock
什么时候该放弃抽象,直接依赖具体驱动
当你的业务逻辑强依赖某驱动特有能力时,硬套抽象层反而增加维护成本。比如:
- 用
pglogrepl做 PostgreSQL 逻辑复制消费,必须操作pgconn.PgConn - 用
mysql.EnableClientDeprecateEOF控制协议版本,这属于mysql.Config字段,无法从sql.DB透出 - 需要调用
pgxpool.Pool.Stat()做连接池监控,而Adapter接口没定义Stat方法
此时更合理的做法是:在仓储实现里直接 import github.com/jackc/pgx/v5/pgxpool,但把具体调用封装在 private 方法中,对外仍保持 StoreUser 这类业务方法签名不变。抽象层不是银弹,它只负责隔离“变”的部分——而驱动能力边界本身就是一种稳定契约。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










