gin.context 不能直接存数据库连接,因其为请求级临时对象,而数据库连接池属长生命周期资源;应存租户标识或工厂函数,通过中间件提取并缓存租户专属 *gorm.db 实例,校验白名单,延迟解析至 dao 层执行时,并避免跨源事务。

为什么 gin.Context 不能直接存数据库连接
因为 gin.Context 是请求生命周期内的临时对象,而数据库连接(尤其是连接池)属于长生命周期资源。把 *sql.DB 或 *gorm.DB 直接塞进 c.Set("db", db),不仅无法复用连接池,还会在每次请求中重复初始化、泄露 goroutine。真正该放进去的,是能按需解析出对应数据源的「标识符」或「工厂函数」。
如何用 gin.Params 或请求头决定数据源
多租户场景下,最轻量的方式是通过 URL 路径参数(如 /tenant/a/orders)或 X-Data-Source 请求头动态选库。关键不是“切换”,而是“延迟解析”——直到 DAO 层真正需要执行 SQL 时才取连接。
- 在中间件里提取标识:
c.Param("tenant_id")或c.GetHeader("X-Data-Source") - 用 map 或 sync.Map 缓存已初始化的
*gorm.DB实例(key 为租户 ID),避免重复 Open - 务必对 tenant_id 做白名单校验,防止 SQL 注入式拼接(比如
"db_" + tenant_id) - 不要在中间件里调
db.WithContext(c.Request.Context())—— 这只是返回新实例,不解决连接归属问题
GORM v2 的 Session 和 Scopes 不适合多数据源
db.Session(&gorm.Session{...}) 只影响当前链式调用的上下文行为(如日志、命名空间),不会改变底层连接池。它不能指向另一个 *gorm.DB 实例。真要换源,必须拿到目标 *gorm.DB 后重新开始链式操作。
- 错误写法:
db.Session(...).Select("name").Where(...).Find(&u)→ 仍走默认 DB - 正确写法:先获取租户专属
tenantDB := getTenantDB(tenantID),再tenantDB.Where(...).Find(&u) - 如果用了
WithContext,注意传入的是c.Request.Context(),不是c本身
事务跨数据源会失败,必须提前约束边界
Gin 里一个 handler 内涉及多个数据源时,无法用单个事务包裹全部操作。MySQL / PostgreSQL 的事务天然绑定到单个连接,跨库 = 跨连接 = 跨事务。这时候得接受最终一致性,或引入 Saga 模式。
- 禁止:在一个
tx := db.Begin()里混用tx.Table("a").Create()和otherDB.Table("b").Create() - 推荐:把跨源操作拆成独立步骤,每个步骤自带重试和补偿逻辑(例如记录
outbox表 + 定时扫描) - 若必须强一致,改用分布式事务框架(如 Seata),但 Gin 层只负责透传 XID,不参与两阶段提交
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











