不能直接在NewData中裸调gorm.Open,因其返回的gorm.DB不实现io.Closer,未显式管理底层sql.DB连接池(需SetMaxOpenConns等)、未透传Kratos日志与上下文,导致连接泄漏、SQL无日志、事务失效。

直接用 GORM 连接数据库是可行的,但必须绕过 Kratos 的默认 database/sql 抽象层,且不能在 NewData 中裸调 gorm.Open 而不处理上下文和生命周期 —— 否则服务启动失败、连接泄漏、事务失效这三类问题必现。
为什么不能直接在 NewData 里写 gorm.Open?
Kratos 的 Data 结构体设计初衷是承载“可关闭资源”,而 gorm.Open 返回的 *gorm.DB 本身不实现 io.Closer;若未显式调用 db.Close(),进程退出时连接不会释放。更关键的是:gorm.DB 是轻量对象(非连接池),真正持有连接的是其内部的 *sql.DB,但 GORM 默认不暴露它。
常见错误现象:
- 服务反复重启后,数据库连接数持续上涨,最终触发
too many connections - 日志里看不到 SQL 打印,
Config.Logger配置无效 —— 因为没传入 Kratos 的log.Logger实例 - 事务方法(如
ExecTx)调用后,子函数里的r.data.db.First(...)仍走主连接,不走事务上下文
NewData 中正确初始化 *gorm.DB 的 4 个必要动作
以 MySQL 为例,internal/data/data.go 中的 NewData 必须包含以下逻辑:
- 用
gorm.Open创建*gorm.DB,驱动用mysql.Open(...)或postgres.Open(...) - 调用
db.SetMaxOpenConns和db.SetMaxIdleConns—— 这两个必须显式设,Kratos 不代劳 - 将 Kratos 的
log.Logger封装为gorm.Logger,例如用logger.New(log.NewWriter(logger), ...) - 返回前,把原始
*sql.DB拿出来调一次SetConnMaxLifetime,否则空闲连接不会自动回收
简短示例(仅关键片段):
sqlDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.New(log.NewWriter(logger), logger.Config{
LogLevel: logger.Info,
}),
})
if err != nil {
return nil, nil, err
}
// 提取底层 *sql.DB 并配置
sqlDBInstance, _ := sqlDB.DB()
sqlDBInstance.SetMaxOpenConns(20)
sqlDBInstance.SetMaxIdleConns(5)
sqlDBInstance.SetConnMaxLifetime(60 * time.Minute)
事务必须用 ExecTx + 上下文透传,不能靠 db.Transaction 原生调用
Kratos 的中间件链和业务方法都依赖 context.Context 传递数据。GORM 原生的 db.Transaction(func(tx *gorm.DB) error) 会新建一个独立上下文,导致:
- 中间件中注入的 traceID、userID 等字段丢失
- repo 方法无法感知当前是否在事务中,
r.data.DB(ctx)返回的仍是主连接 - 嵌套事务(如 A 调 B,B 再调 C)无法复用同一 tx 实例
正确做法是:在 data.go 中实现 ExecTx 方法,并在业务层统一用 txn.ExecTx(ctx, fn);所有 repo 方法内部必须通过 r.data.DB(ctx) 获取 DB 实例 —— 这个方法会从 ctx 中提取已存在的 *gorm.DB,不存在才 fallback 到主连接。
连接字符串里别硬编码密码,用 Kratos 的 conf 解析 + 环境变量覆盖
Kratos 的 conf 包支持 YAML + 环境变量双源加载。不要这样写:
dsn := "user:pass@tcp(127.0.0.1:3306)/mydb?..."
应该定义 conf/conf.yaml:
data:
database:
driver: mysql
source: "${DATABASE_SOURCE:root:123456@tcp(127.0.0.1:3306)/test?parseTime=true}"
然后在 NewData 中读取:
source := c.Data.Database.Source db, err := gorm.Open(mysql.Open(source), ...)
启动时用 DATABASE_SOURCE="user:pwd@tcp(...)" kratos -conf ./conf 即可热切换,避免敏感信息进 Git。
最容易被忽略的一点:GORM 的 *gorm.DB 实例不是线程安全的,但 Kratos 的 Data 是单例。只要确保每次调用 r.data.DB(ctx) 返回的都是同一底层连接池(即没做 db.Session(...) 乱创建新实例),就无需额外加锁 —— 否则性能反而下降。











