应通过依赖注入(如结构体字段)传递*gorm.DB,而非全局变量;需显式配置连接池参数,并用WithContext绑定请求生命周期以保障并发安全、可测可控。

直接在 Fiber 里用 gorm.Open 初始化后全局挂载 DB 实例,能跑通但线上容易 panic 或连接泄漏——这不是集成姿势问题,是没处理好生命周期、并发安全和错误传播路径。
db 必须作为依赖注入进 handler,不能全局变量直用
很多人写 var db *gorm.DB 全局声明,然后在 handler 里直接调用 db.First()。这会导致:
- 并发下
db.Close()可能被误调,后续查询 panic:"invalid connection" - 测试难 mock,handler 和数据库强耦合
- 事务无法按请求粒度控制(比如中间件开启事务,handler 内部却用了另一个 db 实例)
正确做法是把 *gorm.DB 通过 c.Locals 注入,或更推荐:封装成结构体方法传参。
示例:
type UserHandler struct {
db *gorm.DB
}
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
var users []model.User
if err := h.db.Find(&users).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(users)
}
初始化时:
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
app := fiber.New()
handler := &UserHandler{db: db}
app.Get("/users", handler.GetUsers)
连接池配置必须显式设置,否则默认值极不友好
gorm.Open 返回的 *gorm.DB 只是 ORM 层,底层 *sql.DB 的连接池参数默认全是 0 或极小值,高并发下立刻打满或空转。
必须立刻获取并配置:
-
SetMaxIdleConns(10):空闲连接数太少会频繁建连 -
SetMaxOpenConns(100):默认 0 表示无限制,可能耗尽 DB 连接数 -
SetConnMaxLifetime(time.Hour):避免长连接老化失效引发的 "connection refused"
完整初始化片段:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
事务必须用 db.WithContext(c.Context()) 而非 db.Transaction
db.Transaction() 启动的是新 goroutine 级事务,和 fiber.Ctx 生命周期不绑定。一旦 handler 返回或超时,事务不会自动 rollback,且 context cancel 无法透传到底层 SQL。
真正安全的做法是:
- 用
db.WithContext(c.Context())包装查询,让所有操作响应 ctx Done() - 手动 begin/commit/rollback,并 defer 处理异常
示例:
func (h *UserHandler) CreateWithTx(c *fiber.Ctx) error {
tx := h.db.WithContext(c.Context()).Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
return tx.Error
}
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
Preload 关联查询要防 N+1,且不能和 Count 混用
写 db.Preload("Posts").Find(&users) 很方便,但若没加 Joins 或没控制深度,极易触发 N+1;更隐蔽的是,Count() 完全不感知 Preload,直接复用会查错表或漏条件。
关键规则:
- Preload 前先确认外键字段有索引(如
posts.user_id) - 多级 Preload 用
Preload("Profile.Address"),别嵌套调用 -
Count()必须拆开:先db.Model(&User{}).Where(...).Count(),再db.Preload(...).Where(...).Find() - 大数据量优先考虑
Joins().Select()手动拼字段,避免 GORM 自动生成的 LEFT JOIN 性能陷阱
容易被忽略的点:GORM v2 默认关闭 Logger,上线前务必打开日志看实际生成的 SQL,尤其检查是否出现意外的子查询或全表 JOIN。











