直接用database/sql+context切换数据源会出错,因为sql.DB实例固定绑定初始化时的单个连接池,不支持运行时动态路由;context.WithValue无法自动透传或影响底层连接选择,且DB关闭后不可重连,必须为每个数据源独立调用sql.Open并显式选择主/从实例。

为什么直接用 database/sql + context 切换数据源会出错
Go 原生 database/sql 的 DB 实例本身不带“当前路由目标”概念,所有 Query/Exec 都固定打向初始化时传入的单个连接池。你没法在一次请求中临时把某个 QueryRow 发到从库、另一个 Exec 发到主库——除非手动管理多个 *sql.DB 实例并显式选择。
常见错误是试图用一个全局 *sql.DB + 中间件修改连接字符串,结果 panic 报 sql: database is closed 或连接泄漏,因为 DB 关闭后无法重连,且 Open 不是线程安全的。
- 每个数据源必须独立调用
sql.Open,各自维护连接池 - 读写分离不是“动态切换 DB”,而是“按意图选 DB”:读操作走从库池,写操作走主库池
- 不能依赖
context.WithValue在中间件里塞 DB 实例——下游 handler 无法感知,且容易被 cancel 或覆盖
如何设计支持读写的多数据源结构体
核心是封装一组命名的 *sql.DB,并暴露语义化方法,比如 Master() 和 Slave(),而不是让用户自己记哪个变量对应哪台机器。
推荐结构:
立即学习“go语言免费学习笔记(深入)”;
type DBRouter struct {
master *sql.DB
slaves []*sql.DB // 支持多个从库做负载
}
func (r *DBRouter) Master() *sql.DB { return r.master }
func (r *DBRouter) Slave() *sql.DB {
if len(r.slaves) == 0 {
return r.master // fallback,避免 panic
}
return r.slaves[rand.Intn(len(r.slaves))]
}
- 不要用 map[string]*sql.DB 存储——类型丢失,调用时还得类型断言
- 从库列表建议用 slice 而非 map,方便轮询或随机取;如果需要权重,可额外加
[]struct{db *sql.DB; weight int} - 初始化时对每个
*sql.DB调用SetMaxOpenConns和SetConnMaxLifetime,避免主从连接池参数不一致导致从库连接耗尽
怎么让 ORM(如 GORM)也走读写分离
GORM v2+ 提供了 Resolver 接口,但默认只支持按表名路由。要实现“读写意图驱动”,得绕过表名,改用上下文 key 标记意图。
实操做法:
- 定义 context key:
type dbRoleKey struct{},然后用context.WithValue(ctx, dbRoleKey{}, "slave")包裹读请求 - 自定义
gorm.Session的Resolver,在Resolve方法里检查 ctx:if role, ok := ctx.Value(dbRoleKey{}).(string); ok && role == "slave" { return router.Slave() } - 注意:GORM 的
Session不自动透传 context,必须显式用db.WithContext(ctx)启动链路 - 慎用
db.Session(&gorm.Session{NewDB: true})——它会新建 session 但丢掉原始 ctx,导致 resolver 拿不到 key
事务里为什么不能用 Slave(),以及怎么兜底
事务必须绑定单一物理连接,而从库只读,BeginTx 在从库上调用会直接返回 driver.ErrBadConn 或类似错误(取决于驱动)。更隐蔽的问题是:即使没报错,某些 MySQL 从库配置了 read_only=ON,INSERT 也会被拒绝。
- 所有事务操作(
Begin/Commit/Rollback)必须强制走Master() - 可以在
DBRouter上加一层校验:func (r *DBRouter) MustMaster(ctx context.Context) *sql.DB,内部记录是否已在事务中,避免嵌套事务误用从库 - 日志建议打上 source 标签:
log.Printf("query on %s", dbSource),其中dbSource是 "master" 或 "slave-0" - 监控项要区分主从 QPS、慢查询、连接数——从库延迟高时,不能只看主库指标
最易忽略的是连接池健康检查:主库故障时,别让全部流量切到单个从库上压垮它;要有降级开关,比如环境变量 DISABLE_SLAVE=true 时所有读也走主库。


















