应全局单次初始化 *gorm.DB 而非在 handler 中反复调用 gorm.Open;需预设连接池参数、注入结构体字段而非 context、按数据源分变量命名、初始化失败时判空再 defer db.Close()。

初始化 GORM 时别在 handler 里反复调用 gorm.Open
常见错误是每次 HTTP 请求都新建一个 gorm.DB 实例,比如在某个 GET /user 处理函数里写 db, err := gorm.Open(...)。这会导致连接池失控、资源泄漏、性能骤降——gorm.DB 是线程安全的、可复用的句柄,不是一次性的。
正确做法是在项目启动阶段(init 或 main)初始化一次,并作为全局变量或依赖注入到路由/控制器中:
- 用
viper或flag提前读取数据库配置(如database.host、database.port) - 调用
gorm.Open时传入驱动实例(如mysql.Open(dsn)),不是字符串 - 立即配置连接池:
db.DB().SetMaxIdleConns(10)、db.DB().SetMaxOpenConns(50)、db.DB().SetConnMaxLifetime(1h) - 避免漏掉
db.Use(gormlogger.New(...)),否则调试时看不到 SQL 日志
把 *gorm.DB 注入到 Gin 的 gin.Context 里要谨慎
有人喜欢用 c.Set("db", db) 把 DB 实例塞进每个请求上下文,再在 handler 里 c.MustGet("db").(*gorm.DB) 拿出来。这看似方便,但有三个硬伤:
- 类型断言失败时 panic,而不是返回明确错误
- 绕过编译期检查,重构时容易漏改
- 无法做连接健康检查或上下文感知的超时控制(比如你希望某个接口只用读库,但
c.Set塞进去的是主库)
更稳的方式是:把 *gorm.DB 作为结构体字段注入到 controller 实例中,例如:
type UserController struct {
DB *gorm.DB
}
func (uc *UserController) GetUser(c *gin.Context) {
var user User
uc.DB.First(&user, c.Param("id"))
c.JSON(200, user)
}
多数据源场景下,别共用同一个 *gorm.DB 变量名
当项目需要读写分离或对接多个数据库(如 MySQL + PostgreSQL),直接用 var DB *gorm.DB 就会撞名。GORM V2 不再支持 gorm.Open("mysql", ...) 这种字符串驱动方式,必须用具体驱动的 Open 函数。
推荐按数据源分包或分变量命名:
- 写库:
var WriteDB *gorm.DB,用mysql.Open(writeDSN)初始化 - 读库:
var ReadDB *gorm.DB,用mysql.Open(readDSN)初始化 - PostgreSQL 库:
var LogDB *gorm.DB,用postgres.Open(logDSN)初始化 - 每个 DB 都要单独调用
SetMaxOpenConns和SetConnMaxLifetime,不能复用同一套参数
初始化失败时,defer db.Close() 容易被忽略
很多示例代码在 main 函数里初始化 DB 后没做 defer,导致程序退出时连接未释放。但注意:db.Close() 只关闭底层 *sql.DB,不等于“关掉所有连接”——它只是把连接池标记为不可用,已建立的连接会等空闲后自动关闭。
真正关键的是:如果初始化失败(比如 DSN 错误、网络不通),db 是 nil,此时执行 defer db.Close() 会 panic。所以得加判空:
if db != nil {
defer db.Close()
}
更稳妥的做法是把 DB 初始化逻辑封装成函数,返回 *gorm.DB 和 error,并在 main 中统一处理失败与清理。


















