
Go 的 database/sql 接口为什么不是桥接模式
它根本没在实现桥接——桥接模式要求抽象与实现分离,且两者可独立变化;而 database/sql 是典型的「抽象工厂 + 策略」混合体:sql.DB 是高层抽象,driver.Driver 和 driver.Conn 是底层策略接口,但所有驱动必须完整实现整套接口,无法拆分「连接管理」和「语句执行」的实现逻辑。
常见误解是看到 sql.Open("mysql", ...) 就以为这是桥接的“桥接器”,其实这只是工厂函数,背后没有运行时可替换的实现维度。
- 桥接需要两套平行继承体系(比如
Abstraction和Implementor),Go 的database/sql没有抽象基类,也没有实现类继承链 - 所有驱动(
mysql、postgres、sqlite3)都直接实现driver.Driver,不存在「同一抽象适配多种实现变体」的结构 - 如果你强行用桥接思路去封装多数据库,反而会和
sql.Tx、sql.Stmt的生命周期管理冲突
driver.Driver 接口的设计意图与真实约束
这个接口只定义了一个方法:Open(dsn string) (driver.Conn, error),但它承载了整个驱动的初始化契约。它不负责连接池、超时、重试——这些全由 sql.DB 统一处理。
容易踩的坑是:在 Open 里做连接验证或预热,这会导致 sql.Open 调用阻塞,且被 sql.DB 认为“连接已建立”,后续实际执行时仍可能失败。
立即学习“go语言免费学习笔记(深入)”;
-
Open应该轻量:只解析 DSN、初始化配置、返回一个可延后拨号的Conn实例 - 真正建连发生在第一次
Conn.Prepare或Conn.Exec时,由驱动自己控制重试逻辑 - 如果 DSN 格式不一致(比如
mysql用user:pass@tcp(127.0.0.1:3306)/db,而postgres用host=localhost port=5432 dbname=test),别试图在Open里统一解析——交给上层业务或专用 DSN 解析库更稳妥
写一个多后端兼容的 DAO 层,关键不在桥接而在隔离点
你不需要模拟桥接结构,而是要把「SQL 差异」和「驱动行为差异」两个变量分别卡死。
比如 LAST_INSERT_ID() 在 MySQL 里是函数,在 PostgreSQL 里得用 RETURNING id,SQLite 用 last_insert_rowid()。这些不能靠接口抽象掉,必须在调用前判断 db.DriverName()。
- 用
sql.DB.DriverName()做运行时分支,比维护一套“通用 SQL 编译器”更可靠 - 避免在 DAO 方法里拼接 SQL 字符串,改用
sqlx.NamedExec或squirrel这类能按方言生成语句的库 - 事务控制要小心:
sql.Tx不跨驱动保证隔离级别,PostgreSQL 的REPEATABLE READ和 MySQL 的同名级别语义不同,别假设行为一致
自定义驱动时最容易被忽略的 driver.Stmt 生命周期
很多人只关注 Conn,却忘了 Stmt 是有状态的:它可能缓存解析后的查询计划、绑定参数模板、甚至连接上下文。一旦 Conn 关闭,关联的 Stmt 必须失效。
错误现象:invalid memory address or nil pointer dereference 出现在 Stmt.Exec,往往是因为上层复用了已关闭连接创建的 Stmt。
-
Stmt.Close()必须释放所有资源,包括底层协议句柄;不关可能泄露 socket 或内存 -
Stmt.NumInput()返回 -1 表示不支持参数绑定(如某些日志驱动),此时调用Stmt.Exec传参会 panic - 不要在
Stmt内部缓存*sql.DB或其他高层对象——它只属于单个Conn实例
复杂点在于:Go 的 database/sql 会自动复用 Stmt(通过 sql.Stmt 封装),但底层驱动的 Stmt 实例是否线程安全、能否跨 goroutine 复用,完全取决于驱动自己实现。这点文档几乎不提,只能看源码。











