必须为每个数据源独立初始化*sql.DB实例,Go原生不支持运行时切换DSN或驱动;需为不同库(如user_db/order_db)分别sql.Open并单独设置连接池参数,驱动须编译期匿名导入,多租户场景应结合sync.Map缓存+context懒加载租户专属实例。

必须为每个数据源独立初始化 *sql.DB 实例
Go 原生不支持运行时切换 *sql.DB 的底层驱动或 DSN,试图用一个变量反复赋值(比如 db = sql.Open("mysql", dsn1) 后又 db = sql.Open("postgres", dsn2))会导致前一个连接池未关闭、连接泄漏、事务错乱,甚至 panic。哪怕只是两个 MySQL 库(user_db 和 order_db),host/port 完全一致,仅 database 名不同,也必须走两套独立初始化流程:
userDB, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/user_db?")orderDB, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/order_db?")
别用全局 var db *sql.DB 覆盖赋值——Go 不会自动回收旧连接池。每个实例必须用独立变量名,且后续所有连接池参数(SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime)都要单独设置。
驱动注册必须在编译期完成,不能运行时导入
sql: unknown driver "postgres" 这类错误不是配置写错了,而是你没在 main.go 里 blank-import 对应驱动。Go 的 database/sql 驱动靠 init() 函数注册,运行时无法动态加载:
- MySQL:
_ "github.com/go-sql-driver/mysql" - PostgreSQL:
_ "github.com/jackc/pgx/v5/pgxpool"(GORM v2 推荐)或_ "github.com/lib/pq" - SQLite:
_ "github.com/mattn/go-sqlite3"
所有可能用到的驱动都得提前写进 main 包,哪怕某些环境实际不用——否则二进制里根本没注册,sql.Open("postgres", ...) 必 panic。别指望 plugin 或反射绕过这个限制,Go 官方不支持。
立即学习“go语言免费学习笔记(深入)”;
租户/多库路由必须用 sync.Map + context 懒加载
直接预创建所有租户的 *sql.DB 实例既浪费资源又难维护;而每次请求都重连又拖慢响应。正确做法是按需初始化并缓存:
- 主库(
masterDB)只查tenants表,拿到租户的db_host、db_name等元信息 - 用
sync.Map缓存租户 ID →*sql.DB映射:tenantDBs.LoadOrStore(tenantID, newDB(cfg)) - HTTP 中间件从
r.Header.Get("X-Tenant-ID")提取标识,查主库验证后注入context:ctx = context.WithValue(ctx, tenantDBKey, db) - Handler 中必须显式取值:
db := ctx.Value(tenantDBKey).(*sql.DB),绝不能 fallback 到全局变量
若租户配置变更(如 DB 地址更新),需调用 LoadAndDelete + 重建,而非热 reload 连接池——后者无法保证旧连接彻底释放。
读写分离不能靠 SQL 文本解析,得靠 context 显式标记
SELECT 开头不等于读操作,SELECT ... FOR UPDATE 是写锁,走从库会直接报错(MySQL 报 ERROR 1290,PostgreSQL 报 cannot execute SELECT FOR UPDATE in a read-only transaction)。正则匹配 SQL 前缀极易漏判(注释包裹、大小写混用、子查询嵌套)。
安全做法是业务层主动透传意图:
- 写操作:用
context.WithValue(ctx, "rw_hint", "write") - 强一致性读:同样标
"write",确保走主库 - 普通读:不设或标
"read",由路由层分发到从库 - 事务启动统一用
BeginTx(ctx, &sql.TxOptions{ReadOnly: false}),此时无论 SQL 内容如何,都强制走主库
多个从库之间轮询时,每个都得是独立 *sql.DB 实例,连接池隔离;用原子计数器选实例,加健康检查(失败后临时剔除,5 秒后重试),避免单点拖垮整体读性能。


















