不能直接多次调用 sql.open 创建连接池,因为每个 sql.db 是独立连接池,会导致文件描述符耗尽、dns/tls 重复开销、无法复用连接及跨库事务失效;应统一管理 dbrouter 并注入 context 实现运行时路由。

为什么不能直接用 sql.Open 多次创建连接池?
很多人第一反应是:每个数据库开一个 sql.DB 实例,按需调用。这在简单场景下能跑通,但很快会暴露问题——sql.DB 本身是连接池,不是单连接;频繁新建 sql.DB 实例会导致文件描述符耗尽、DNS 重复解析、TLS 握手开销剧增,且无法共享连接复用逻辑。
更关键的是,事务无法跨 sql.DB 实例传播,一旦需要跨库事务(哪怕只是读一致性快照),这条路就走不通。
- 每个
sql.DB独立维护自己的空闲连接、最大连接数、健康检查周期 - 没有统一的上下文透传机制,中间件(如 tracing、timeout)难以注入到所有数据源
- 测试时 mock 成本高:要 mock 多个不同 DB 实例,而非一个可切换的抽象
用 context.Context + 自定义 DBRouter 实现运行时路由
核心思路是把数据源选择逻辑从“初始化时决定”移到“执行前一刻决定”,靠 context.Context 携带目标库标识(如 "tenant_id=shanghai" 或 "role=replica"),由统一的 DBRouter 解析并返回对应 *sql.DB。
示例结构:
type DBRouter struct {
masters map[string]*sql.DB // key: "shanghai", "beijing"
replicas map[string]*sql.DB
defaultDB *sql.DB
}
<p>func (r <em>DBRouter) Get(ctx context.Context) (</em>sql.DB, error) {
tenant := ctx.Value("tenant").(string)
if db, ok := r.masters[tenant]; ok {
return db, nil
}
return r.defaultDB, nil
}
</p>
- 避免全局变量:把
DBRouter作为依赖注入进 handler/service,方便单元测试替换 - 不要在
Get()里做 DNS 查询或建连:所有*sql.DB必须提前Open并完成Ping()验证 - 如果路由键可能为空或非法,
Get()必须有明确 fallback 行为(比如 panic、log+default、error 返回),不能静默降级
sql.Tx 跨库失败时的真实错误信息长什么样?
Go 标准库不支持跨 *sql.DB 的事务。当你试图对两个不同 *sql.DB 实例分别调用 Begin(),再各自 Commit(),这不是分布式事务,只是两段独立事务——中间任意一个失败,另一个已提交,数据必然不一致。
典型错误现象:
- 没报错,但业务数据对不上(最危险)
- 手动实现两阶段提交时,第二阶段
Commit()报"sql: transaction has already been committed or rolled back" - 使用
pgxpool时,若误将不同 pool 的 conn 传入同一tx,会触发"conn is not part of this tx"
解决方向只有两个:要么接受最终一致性(用消息队列补偿),要么引入真正支持 XA 或 SAGA 的中间件(如 distributed-sql、vitess),Go 层只做路由,不碰跨库事务语义。
切库逻辑放在哪一层最容易维护?
放在 DAO 层最稳。HTTP handler 或 service 层只负责把路由键(如租户 ID、地域标签)塞进 context.WithValue(),DAO 层的 QueryRowContext() / ExecContext() 调用前,统一由 DBRouter.Get(ctx) 拿连接。
- 避免在 model struct 里硬编码库名(如
db:"shanghai_users"),这会让迁移和灰度发布变得脆弱 - 不要在 SQL 字符串里拼库名(
"SELECT * FROM shanghai.users"),MySQL 8.0+ 对跨库查询权限管控更严,且无法利用 prepared statement 缓存 - 如果用了 ORM(如
gorm),务必关掉其自带的多库支持(gorm.Config.Dialector不应动态切换),否则会和你的路由逻辑冲突
真正的难点从来不在“怎么切”,而在于“切完之后,连接生命周期、日志 traceID、panic 恢复、metric 打点是否还保持一致”。这些细节没对齐,切库就只是把问题从一个地方挪到另一个地方。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











