Gin中间件无法实现可靠读写分离,因其仅能拦截HTTP请求,无法感知SQL执行意图(如事务内SELECT、SELECT FOR UPDATE等),易导致主从数据不一致;应使用自定义database/sql driver在连接层透明路由。

Gin 本身不提供主从路由能力,必须靠数据库驱动层或中间件控制连接分发;直接在 Gin 中硬编码 dbMaster / dbSlave 变量再手动选库,短期可行,但长期会失控。
为什么不能只靠 Gin 的中间件做读写分离
Gin 的中间件只能拦截 HTTP 请求,无法感知 SQL 执行意图(比如 SELECT 是否带锁、是否在事务中)。你写一个 ReadWriteMiddleware,它没法判断:
- 当前请求里有没有开启事务(事务内所有语句都该走主库)
- ORM 生成的
SELECT ... FOR UPDATE是读还是写 - 第三方库(如
sqlc、ent)发出的查询是否被中间件捕获
结果就是:中间件把 SELECT 转给从库,但某次事务里的 SELECT 被错误分流,导致读到过期数据。
推荐用 sql.DB + driver.Connector 实现透明路由
真正可控的方式是让连接池自己决定用哪个 DB 实例。核心思路是封装一个自定义 driver.Driver,在 OpenConnector() 时根据上下文返回主库或从库的 connector:
type RoutingDriver struct {
master *sql.DB
slaves []*sql.DB
}
func (r *RoutingDriver) OpenConnector(name string) (driver.Connector, error) {
// 根据 goroutine local context 或调用栈判断读/写意图
if isWriteContext() {
return r.master.Connector(), nil
}
// 轮询或权重随机选一个 slave
idx := rand.Intn(len(r.slaves))
return r.slaves[idx].Connector(), nil
}
然后注册为默认 driver:
sql.Register("mysql-routing", &RoutingDriver{
master: dbMaster,
slaves: []*sql.DB{dbSlave1, dbSlave2},
})
db, _ := sql.Open("mysql-routing", "")
这样所有基于 database/sql 的 ORM 或原生 db.Query 都自动受控——无需改业务代码,也绕过了 Gin 中间件的语义盲区。
sharding-jdbc-spring-boot-starter 不适用于 Go
你可能在 Java 项目里见过 sharding-jdbc 这类代理式读写分离组件,但它依赖 JVM 字节码增强和 Spring AOP,Go 没有等效机制。试图用 Go 模拟类似逻辑(比如 hook database/sql 的 Query 方法)会遇到:
- 无法拦截
Stmt.Query或Rows.Scan等底层调用 - 反射替换
sql.DB内部字段极不稳定,Go 1.20+ 已禁止修改未导出字段 - goroutine 复用导致上下文污染(比如前一个请求设了“读”,后一个请求误继承)
所以别碰“动态代理”思路。Go 生态更务实的做法是:在 DAO 层显式调用 masterDB.Exec 或 slaveDB.Query,配合 interface 抽象隔离变化。
容易被忽略的三个细节
主从配置最常崩在边界场景,不是连不上,而是“看起来连上了,但数据不对”:
-
read-only=1必须在从库 my.cnf 里配死,不能只靠应用层约定;否则运维误操作写入从库,后续同步直接报错duplicate entry - GTID 模式下,
CHANGE MASTER TO ... MASTER_AUTO_POSITION = 1是刚需;用传统MASTER_LOG_FILE/MASTER_LOG_POS在主库 failover 后极易断链 - Gin 启动时要预热连接池:
db.Ping()只检查主库,得对每个slaveDB单独Ping(),否则第一个读请求超时才暴露从库不可达


















