不能在 Handler 里用 if-else 切数据库,因为 Go 的 sql.DB 不支持运行时切换底层 DSN,仅变量赋值会导致连接池共享、事务错乱、连接泄漏及 panic;必须用 sync.Map 按租户 ID 缓存独立 sql.DB 实例,并通过 Gin 中间件注入 context,Handler 显式从 ctx 获取。

为什么不能在 Handler 里用 if-else 切数据库
直接在 GET 或 POST 处理函数里写 if tenant == "a" { db = aDB } else { db = bDB } 是危险的。Go 的 *sql.DB 不支持运行时切换底层 DSN,这种写法只是在变量层面赋值,所有请求仍共用同一个连接池实例,会导致事务错乱、连接泄漏,甚至 panic: sql: database is closed。更严重的是,它完全绕过了连接池隔离——比如租户 A 的慢查询会拖垮租户 B 的连接数。
必须用 sync.Map 缓存租户专属 *sql.DB 实例
每个租户对应一个独立初始化的 *sql.DB,不能复用全局变量。正确做法是用 sync.Map 按租户 ID 缓存已配置好连接池的实例:
- 键为字符串(如
r.Header.Get("X-Tenant-ID")),值为*sql.DB - 首次访问某租户时才调用
sql.Open初始化,并立即设置SetMaxOpenConns、SetConnMaxLifetime - 避免启动时全量加载:冷租户不占内存、不建空连接
- 主库只用于查
tenants表验证租户有效性,不参与业务读写
Gin 中间件必须在 c.Next() 前注入 context.Context
数据库实例必须在路由匹配后、Handler 执行前注入上下文,否则 Handler 拿不到正确的 *sql.DB。典型错误是中间件里只做校验没塞值,或塞了但 Handler 忘了从 ctx.Value(tenantDBKey) 取:
- 中间件中提取
X-Tenant-ID→ 查主库确认存在 → 从sync.Map获取或新建*sql.DB→ 调用c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), tenantDBKey, db)) - Handler 中必须用
db := c.MustGet(tenantDBKey).(*sql.DB),而不是直接引用全局变量 - 别用
c.Set():它只对 Gin 内部生效,无法穿透到下游封装层(如自定义 Repository)
事务跨库无效,业务代码必须显式控制
userDB.BeginTx() 里的操作只作用于 userDB,调用 logDB.Exec() 会脱离该事务。这不是框架限制,而是 SQL 协议本身决定的:
- 不存在“跨库事务”,MySQL/PostgreSQL 都不支持
- 需要分布式事务时,改用 Saga 模式或消息队列补偿
- 读写分离必须由业务代码决定:比如写用
primaryDB,读用replicaDB,不能靠中间件自动路由 - 接口设计上,让 Handler 显式接收
Queryer接口而非具体*sql.DB,降低耦合
db 能不能用”。


















