go 的 sql.db 不支持自动读写分离,需手动封装路由层:根据操作类型(如 select 或 insert/update/delete)或显式事务意图(如 readonly=false)分发到主库或从库实例,并为多从库实现带健康检查的轮询负载均衡。

怎么让 sql.DB 实例自动走主库或从库
Go 本身没有内置读写分离逻辑,sql.DB 只是单数据源抽象。必须自己封装路由层,在执行前判断操作类型(SELECT 还是 INSERT/UPDATE/DELETE),再分发到对应 *sql.DB 实例。
常见错误是试图用一个 sql.DB 连接字符串拼出“主从地址”,这行不通——MySQL 或 PostgreSQL 驱动不识别这种语法,会直接报错 dial tcp: lookup master,slave: no such host。
- 主库实例:只用于写操作和强一致性读(如刚写完立刻查)
- 从库实例:仅用于普通
SELECT,且需容忍秒级延迟 - 路由判断不能只看 SQL 前缀——有些 ORM 生成的语句以
SELECT开头但实际是写操作(如SELECT ... FOR UPDATE),这类必须走主库
如何安全处理 SELECT ... FOR UPDATE 这类“读中带写”语句
这类语句本质是写锁操作,走从库会报错(MySQL 报 ERROR 1290 (HY000): The MySQL server is running with the --read-only option;PostgreSQL 报 ERROR: cannot execute SELECT FOR UPDATE in a read-only transaction)。
不能靠正则匹配 FOR UPDATE 就放行——因为 SQL 可能被注释包裹、大小写混用、或嵌套在子查询里。更稳妥的方式是显式标记事务意图:
立即学习“go语言免费学习笔记(深入)”;
- 用
context.WithValue(ctx, "rw_hint", "write")在调用前透传写意图 - 或统一用
BeginTx(ctx, &sql.TxOptions{ReadOnly: false})启动事务,此时无论 SQL 是什么,都强制走主库 - 避免在 DAO 层做 SQL 文本解析,维护成本高且易漏
多个从库之间怎么负载均衡
简单轮询或随机选一个从库是最常见做法,但要注意连接池隔离——每个 *sql.DB 实例自带独立连接池,不能共用。
容易踩的坑是把所有从库塞进一个 sql.DB 并期望它自动分发,结果只是连上了第一个地址,其余配置完全被忽略。
- 为每个从库创建独立
*sql.DB实例,并设置合理SetMaxOpenConns(比如总连接数 / 从库数) - 轮询时用原子计数器而非
rand.Intn,避免并发下重复选同一个实例 - 加上健康检查:某从库不可用时临时剔除,5 秒后重试,别让它拖慢整个读请求
为什么 database/sql 的 QueryRow 和 Exec 不能混用数据源
因为 QueryRow 默认走只读路径,Exec 默认走写路径,但它们底层都调用 db.conn() 获取连接——而这个方法不关心你是读还是写,只看你传入的是哪个 *sql.DB 实例。
所以如果你把主库的 *sql.DB 实例误传给本该走从库的 QueryRow,它照样执行,只是白白消耗主库资源;反过来,用从库实例调 Exec 则必然失败。
- 最简单的隔离方式:定义两个包级变量
var MasterDB *sql.DB和var SlaveDB *sql.DB,并在 DAO 函数签名里明确要求传入对应实例 - 或者封装一层
type DBRouter struct{ master, slaves []*sql.DB },暴露Query/Exec方法内部自动路由 - 别依赖函数名猜行为,
GetUserByID看似只读,但如果它内部要更新最后访问时间,就必须走主库
真正麻烦的从来不是配几个数据源,而是业务逻辑里那些“看起来是读、其实要写”的边界情况——它们不会报错,但会让主库压力飙升,还可能引发数据不一致。










