不能将 *gorm.db 当全局单例注入所有 service,因其是可变上下文、非线程安全,高并发下易导致条件污染、查空、panic;正确做法是每次请求传入干净实例,repository 不持有、不缓存,事务由 service 统一管控。

直接复用 GORM 的 *gorm.DB 实例做通用数据访问层,在微服务里大概率会出竞态、连接泄漏或条件污染——不是不能用,是必须拆开重装。
为什么不能把 *gorm.DB 当全局单例注入所有 Service
微服务里并发高、事务边界多,*gorm.DB 是非线程安全的链式构建器,不是连接池本身。你把它塞进 Service 结构体并反复调用 Where()、Joins(),实际是在改它内部的 Statement 状态。两个 goroutine 同时操作同一个 *gorm.DB,一个加了 WHERE deleted_at IS NULL,另一个刚调完 Count() 就被污染了 GROUP BY,结果 Find() 查不到数据,日志里还看不出 SQL 差在哪。
- 现象:Service 方法在压测时偶发查空、Total 错乱、panic 报
invalid memory address - 根本原因:GORM v2 的
*gorm.DB是“可变上下文”,不是“只读查询模板” - 正确姿势:每个请求入口(如 Gin handler)拿到的是干净的
*gorm.DB,用完即弃;Service 层只接收*gorm.DB作为参数,不持有、不缓存
怎么封装 Repository 接口才真正解耦
Repository 不是包名,是一组明确契约:输入 domain struct 或 ID,输出 domain struct 或 error,不暴露 *gorm.DB、不拼 SQL、不处理事务。
- 定义接口时,方法签名只含业务语义,比如
FindByEmail(ctx context.Context, email string) (*User, error),而不是Get(ctx context.Context, db *gorm.DB, cond map[string]interface{}) - 实现类里,每次调用都基于传入的
*gorm.DB新建干净实例:db.Session(&gorm.Session{NewDB: true}),尤其涉及Count()+Find()组合时 - 避免在 Repository 里写分页逻辑——分页是 controller 或 service 编排层的事,Repository 只负责“按条件查一批”和“按条件数总数”两个原子动作
连接池和模型初始化必须脱离 Repository 生命周期
数据库连接池(sql.DB)和 GORM 初始化(gorm.Open)是进程级资源,必须在应用启动时完成,且只做一次。Repository 层绝不参与 Open、SetMaxOpenConns 这类操作。
- 连接池配置必须显式设:
db.DB().SetMaxIdleConns(10)、db.DB().SetMaxOpenConns(50)、db.DB().SetConnMaxLifetime(30 * time.Minute),硬编码值要根据服务 QPS 和 DB 规格动态调整 - 模型定义必须显式 tag:所有字段禁用“约定优于配置”,
ID uint `gorm:"primaryKey;column:id"`、CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null"`,否则跨团队协作或表结构变更时字段错位无声无息 - AutoMigrate 必须禁用在生产环境:它只适合本地开发,上线后靠 SQL 迁移脚本或 Flyway/Liquibase 管控
事务控制必须收归 Service 层,Repository 不感知
Repository 方法签名里不能出现 tx *gorm.DB 参数,也不该有 Begin() / Commit() 调用。事务是业务编排的边界,不是数据访问的细节。
- Service 函数内统一用
db.Transaction(func(tx *gorm.DB) error { ... })包裹多个 Repository 调用 - 事务函数体内,每个 Repository 方法都接收
tx作为参数,且立即用tx.Session(&gorm.Session{NewDB: true})派生新实例,防止条件污染 - 别信
defer tx.Rollback():它无法捕获协程 panic,也无法处理嵌套事务回滚失败,必须靠返回 error 并由外层判断是否 Commit
最易被忽略的一点:GORM 的 Preload 在微服务里极易触发 N+1,但很多人只在 Repository 里加个 Preload("Profile") 就以为搞定了。实际上,Preload 的 SQL 是在 Find 时才生成,如果 Service 调用了两次 Repository 方法(比如先查用户再查权限),哪怕用的是同一个 *gorm.DB,Preload 也不会复用——这需要你在更高层做 JOIN 显式关联,或者用 DataLoader 模式聚合请求。这不是封装够不够的问题,是数据访问粒度是否匹配业务场景的问题。











