必须为每个数据源独立初始化 *sql.DB 实例,Go 原生不支持运行时切换数据库驱动或 DSN;同一 MySQL 实例下的 user_db 和 order_db 也需分别调用 sql.Open 初始化,禁用全局 var db 赋值覆盖,否则导致连接泄漏和健康检测失效。

多数据源必须各自独立初始化 sql.DB 实例
Go 原生不支持运行时切换数据库驱动或 DSN,所谓“一个 *sql.DB 切多个库”是确定会出问题的。哪怕只是同一 MySQL 实例下的两个库(user_db 和 order_db),也必须分别调用 sql.Open:
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 赋值覆盖——前一个库的连接池不会自动关闭,连接持续泄漏;db.Ping() 也只检测最后一次赋值的实例,其余库处于“黑盒”状态。
读写分离不能靠 SQL 注释,得靠显式路由封装
database/sql 不解析 SQL 语义,/*+ read_only */ 这类注释对 Go 驱动无效,也不能触发中间件自动路由。你得自己控制用哪个 *sql.DB 实例:
- 主库实例只用于
Exec、Begin和事务内所有操作 - 从库实例只用于
Query、QueryRow等只读操作 - 推荐封装
DBRouter结构体,提供WriteDB()/ReadDB(ctx)方法,内部按 context.Value 或显式 flag 选实例
事务中混用从库读取,大概率触发 ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction,且无法保证刚写入的数据被读到。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
租户隔离必须用 sync.Map + context 动态加载
硬编码 if tenant == "a" { return userDB } 会导致每次加新租户都要改业务逻辑。正确做法是:
- 入口中间件从
r.Header.Get("X-Tenant-ID")提取标识,查主库确认有效性 - 调用
tenantDBs.LoadOrStore(tenantID, newDB(cfg)),其中tenantDBs是sync.Map类型 - handler 中通过
ctx.Value(tenantDBKey).(*sql.DB)获取实例,不 fallback 到全局变量
sync.Map 的读路径无锁,适合租户 ID 高频读取、低频新增的场景;但注意它不支持遍历或原子删除,清理过期租户需额外维护时间戳或 TTL 机制。
驱动注册和连接池参数不能省略或复用
报错 sql: unknown driver "postgres" 不是配置错了,而是没在 main.go 里 blank-import 驱动:
- MySQL:
_ "github.com/go-sql-driver/mysql" - PostgreSQL:
_ "github.com/jackc/pgx/v5/pgxpool"(GORM v2 推荐)
连接池参数必须按库单独设置:
- 主库:设
SetMaxIdleConns(5)防止故障恢复慢,SetConnMaxLifetime(5 * time.Minute) - 日志库:可设
SetConnMaxLifetime(30 * time.Minute),因查询少、连接生命周期长 - 漏设
SetMaxOpenConns默认为 0(无上限),数据库容易被打满
最易忽略的是:所有驱动必须在编译期导入,运行时无法动态加载新驱动——哪怕某个环境暂时不用 PostgreSQL,也得提前写进 main.go。

















