database/sql驱动必须在main包init阶段通过空白导入完成注册,运行时无法动态加载;多数据源需提前导入所有可能驱动,再通过配置解析和sql.Open分别构建独立连接池。

database/sql 的驱动注册必须在 main 包 init 阶段完成
Go 的 database/sql 不自带驱动,所有驱动都靠 sql.Register() 注册后才能用。而注册动作发生在驱动包的 init() 函数里——这意味着你不能在运行时(比如根据 flag 或配置)再去 import 某个驱动包并期望它“即时生效”。_ "github.com/go-sql-driver/mysql" 这类空白导入必须出现在编译期可见的代码路径中,否则 sql.Open("mysql", ...) 会 panic:unknown driver "mysql"。
常见错误现象:sql: unknown driver "postgres" (forgotten import?) —— 不是配置写错了,而是对应驱动没被编译进去。
- 所有可能用到的驱动(如
mysql、postgres、sqlite3、dm)都得提前 blank-import 到main.go或某个会被编译进最终二进制的包里 - 不要试图用
plugin或go:embed动态加载驱动;Go 官方不支持运行时注入驱动实现 - 如果项目只部署 MySQL,但代码里 import 了 PostgreSQL 驱动,不会增加运行时开销,只是二进制体积略大
用 viper + struct tag 解析多数据源配置并构造多个 *sql.DB 实例
驱动注册完只是第一步,真正实现“动态选择”靠的是配置解析和连接池构建。推荐用 viper 加载 YAML/JSON/TOML,把不同数据库的 driver 名(如 "mysql")、dsn、连接参数分组管理。
示例配置片段(config.yaml):
database: - name: primary driver: mysql dsn: root:pass@tcp(127.0.0.1:3306)/app?parseTime=true&loc=Local max_open_conns: 50 - name: analytics driver: postgres dsn: host=localhost port=5432 user=app dbname=report sslmode=disable max_open_conns: 20
关键点:
-
driver字段值必须和sql.Register()时传入的 name 严格一致(大小写敏感) - 每个数据源需独立调用
sql.Open(),返回各自的*sql.DB;别共用一个实例 - 务必设置
SetMaxOpenConns和SetMaxIdleConns,否则默认 0(不限制)容易打爆数据库 - 建议为每个
*sql.DB实例加前缀日志或封装成带名称的 struct,避免后续混淆
GORM v2 多数据源需显式传入不同驱动的 Dialector
如果你用 GORM,v2 的 gorm.Open() 不再接受字符串 driver name,而是要求传具体驱动的 Dialector 实例。这意味着你不能仅靠配置里的 driver: "mysql" 就自动选型——得手动匹配。
典型做法是写一个映射函数:
func NewDialector(cfg DatabaseConfig) gorm.Dialector {
switch cfg.Driver {
case "mysql":
return mysql.Open(cfg.DSN)
case "postgres":
return postgres.Open(cfg.DSN)
case "sqlite":
return sqlite.Open(cfg.DSN)
default:
panic("unsupported driver: " + cfg.Driver)
}
}注意:
- 每个驱动对应的
gorm.io/driver/xxx包必须已go get并 blank-import(同database/sql要求) -
mysql.Open()返回的是mysql.Dialector,不是*sql.DB;GORM 内部会用它创建连接池 - 别把多个数据源的
*gorm.DB实例存在同一个全局变量里;建议用 map[string]*gorm.DB 或结构体字段区分
运行时切换数据源要靠配置热重载 + 连接池重建
真正的“动态”不是指换 driver name,而是指不重启进程就能切换到另一套数据库连接。这需要两件事:配置能实时变更(如文件 watcher),以及旧连接池能安全关闭、新连接池能无缝接管。
可行路径:
- 用
fsnotify监听 config 文件变化,触发 reload 流程 - 对每个数据源维护一个
sync.RWMutex+ 当前*sql.DB指针 - reload 时先调用旧
*sql.DB.Close(),再sql.Open()新实例,最后原子替换指针 - 业务层访问前加
RLock(),确保不会在替换瞬间拿到 nil 或已关闭的句柄
容易被忽略的点:连接池关闭是异步的,Close() 返回后仍可能有活跃连接在归还;生产环境务必加超时等待或 graceful shutdown 逻辑。



















