必须为每个数据源独立初始化*sql.DB实例,因Go原生不支持运行时切换数据库或驱动,复用变量会导致事务错乱、连接泄漏或panic;各实例需单独配置连接池参数并用语义化变量名区分。

多个 *sql.DB 实例必须独立初始化,不能复用变量
Go 原生不支持运行时切换数据库或驱动,sql.Open 返回的是连接池抽象,不是单个连接。你无法靠 db.Exec("USE other_db") 切库——这条语句只影响当前拿到的那个底层连接,下次查询可能落到另一个连着旧库的连接上,查错表、事务失效都是必然结果。
哪怕两个 MySQL 库只差一个 database 名(比如 user_db 和 order_db),host/port 完全一样,也必须走两套初始化:
userDB, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
log.Fatal(err)
}
orderDB, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/order_db?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
log.Fatal(err)
}- 别用全局
var db *sql.DB反复赋值——前一个库的连接池不会自动关闭,连接持续泄漏 - 变量名要有语义,如
userDB、logDB、reportDB,避免歧义 - DSN 中的
host、port、database都要写全,主从场景下从库 DSN 的host必须指向从库地址
驱动注册漏掉 _ 会导致静默失败
sql.Open 或 gorm.Open 不会立刻校验驱动是否存在,错误往往卡在第一次 Query 或 Ping 时,报错像 sql: unknown driver "mysql" 或更隐蔽的 invalid connection。
必须在 main.go(或入口包)里显式 blank-import 对应驱动:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // MySQL
_ "github.com/lib/pq" // PostgreSQL
_ "github.com/mattn/go-sqlite3" // SQLite
)- 漏掉下划线
_就等于没注册,驱动不会被database/sql识别 - 所有可能用到的驱动都得提前写进
main.go,运行时无法动态加载新驱动 - GORM 用户注意:
import _ "gorm.io/driver/mysql"同样必须存在,且和gorm.Open传入的 dialect 严格匹配
连接池参数必须按库单独调优
主库、从库、日志库的流量特征差异极大,共用一套 SetMaxOpenConns 极易引发雪崩:主库突发写入耗尽全部连接,从库查询排队阻塞。
每个 *sql.DB 实例都要单独设置:
userDB.SetMaxOpenConns(50) userDB.SetMaxIdleConns(10) userDB.SetConnMaxLifetime(5 * time.Minute) <p>logDB.SetMaxOpenConns(10) // 日志库读多写少,连接数可设低些 logDB.SetMaxIdleConns(5) logDB.SetConnMaxLifetime(10 * time.Minute)
-
SetMaxOpenConns默认为 0(无上限),务必显式设值,否则数据库容易被打满 -
SetConnMaxLifetime要结合数据库侧的 wait_timeout 设置,避免应用层拿着已断开的连接继续用 - 不要在 init 函数里初始化 DB——错误难捕获;改用显式初始化函数并返回
error
租户/读写路由必须靠 sync.Map + context,不能硬编码 if-else
把数据库选择逻辑塞进 handler 或 DAO 层,等于把路由和业务耦死。加个新租户、切个主从,就得翻出几十个文件改 if tenant == "a" { use userDB }。
推荐做法:
- 中间件从
r.Header.Get("X-Tenant-ID")提取标识,查主库确认有效性后,调用tenantDBs.LoadOrStore(tenantID, newDB(cfg)) - handler 中通过
ctx.Value(tenantDBKey).(*sql.DB)拿实例,不 fallback 到全局变量 - 读写分离靠显式标记:
ctx = context.WithValue(ctx, "rw_hint", "write"),再由路由逻辑决定用哪个实例
最常被忽略的一点:GORM 的 Transaction 只作用于调用它的那个 *gorm.DB 实例,跨实例调用完全游离于事务外——这不是配置问题,是设计边界。需要跨库原子性,只能靠业务层补偿,不是靠调大连接池能解决的。

















