Go 无原生分库分表支持,需依赖中间件或手动路由;推荐在 DAO 层实现分片逻辑,注意表名动态计算、连接预初始化、规避跨库事务,并用 Snowflake 等方案生成全局 ID。

分库分表不是 Go 语言原生能力,得靠中间件或手动路由
Go 本身没有内置的分库分表支持,database/sql 只面向单个 *sql.DB 实例。所有分片逻辑必须由业务代码或第三方库承担。常见错误是试图用一个 sql.DB 连接多个物理库——这行不通,连接池无法跨实例协调事务和连接生命周期。
实际落地时有两种主流路径:
- 用代理层(如
ShardingSphere-Proxy或MyCat),Go 应用无感,但失去对分片键、绑定表、分布式事务的细粒度控制 - 在 Go 服务内做分片路由,推荐使用
shard(轻量)、vitess的 Go client(重但成熟),或自研简单路由层(适合规则固定、分片数少的场景)
手动分片路由:按分片键选择 *sql.DB 实例
核心是把分库分表逻辑下沉到 DAO 层,避免污染业务逻辑。典型做法是维护一个 map[string]*sql.DB(key 是库名/表后缀),再写一个路由函数:
func getDB(shardKey string) *sql.DB {
shardID := hashMod(shardKey, 4) // 假设 4 个库
return dbMap[fmt.Sprintf("user_db_%d", shardID)]
}
func getUserByID(ctx context.Context, userID int64) (User, error) {
db := getDB(strconv.FormatInt(userID, 10))
var u User
err := db.QueryRowContext(ctx, "SELECT FROM user_0001 WHERE id = ?", userID).Scan(&u.ID, &u.Name)
return &u, err
}
注意点:
立即学习“go语言免费学习笔记(深入)”;
- 表名不能硬编码,需根据
userID % 100算出具体表后缀(如user_0023),否则跨表查询会失败 -
getDB返回的*sql.DB必须提前初始化并完成健康检查,不能每次调用都新建连接 - 不支持跨分片
JOIN或全局ORDER BY,这类需求得靠应用层归并或引入 ES
事务一致性只能靠应用层补偿或两阶段提交(2PC)
Go 的 sql.Tx 天然绑定单个 *sql.DB,跨库事务无法用原生事务保证 ACID。常见错误是以为开启一个 tx 就能操作多个分库——实际只会作用于第一个 db.Begin() 的实例。
可行方案有:
- 尽量规避跨库事务:例如用户余额扣减和订单创建放在同一分片(用
userID路由),这是最简单也最常用的解法 - 最终一致性 + 补偿事务:记录本地事务日志,异步发 MQ 触发其他分片操作,失败时查日志重试
- 引入
seata-golang或vitess的vttablet支持的 XA 协议,但会显著增加部署复杂度和延迟
分页、COUNT 和全局唯一 ID 是最容易翻车的三个点
分页查询 LIMIT 20 OFFSET 1000 在分库后变成每个库都返回 1000+20 条,应用层合并再截取——性能爆炸且结果不准。同理,COUNT(*) 需要遍历所有分片求和,无法走索引优化。
全局 ID 推荐用 sony/fluency(Snowflake 变种)或 tidb/tikv 的 alloc 接口,别用 AUTO_INCREMENT 或 UUID —— 前者单点瓶颈,后者导致二级索引碎片化严重。
真正难的不是写分片逻辑,而是让所有开发意识到:“这个 SQL 看似简单,但它在分库后可能触发 8 个数据库实例、16 张表、3 次网络往返”。没做分片前的慢查询,分片后可能更慢。










