gorm automigrate仅安全补全表结构(建表、加字段、扩类型、加索引/外键),不删字段、不改名、不降not null,故生产环境必须用golang-migrate等版本化工具替代。

panic("failed to connect database") 或查不到数据。
为什么 DB.AutoMigrate() 有时不生效
常见现象:改了模型字段,重启服务后数据库表没更新,甚至字段被删了还留着旧数据。
根本原因不是 GORM 不干活,而是 AutoMigrate 只做“新增字段/索引/约束”,不会删字段、改类型、重命名——它本质是安全型迁移,不是同步工具。
实操建议:
- 开发阶段可配合
DB.Migrator().DropTable(&Task{})清掉旧表再跑AutoMigrate(仅限本地或测试环境) - 生产环境必须用显式 migration 文件(如
gormigrate库),靠AutoMigrate上线等于埋雷 - 注意结构体 tag:如果加了
gorm:"column:task_title"却忘了在字段名里同步,AutoMigrate会新建一个默认字段,而不是映射到已有列
gorm.Open() 的 DSN 里这几个参数不能省
MySQL 连接字符串看着长,但漏掉任意一个都可能让时间字段错乱、中文变问号、或连接池卡死:user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
-
charset=utf8mb4:不用utf8,后者在 MySQL 里只是utf8mb3,存不了 emoji 和部分生僻汉字 -
parseTime=True:否则time.Time字段会读成字符串,后续调用.AddDate()直接 panic -
loc=Local:避免时区错位,尤其当服务器和数据库不在同一时区时;若数据库用 UTC,这里得换成loc=UTC
全局 DB 变量怎么设才不踩坑
很多人把 var DB *gorm.DB 放顶层,init 函数里初始化——看似简洁,实则隐藏三个问题:并发初始化竞争、无法注入 mock DB 做单元测试、DB 配置无法热更新。
更稳妥的做法是封装成函数返回:
func NewDB(cfg config.Database) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.UserName, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent), // 生产关日志
})
if err != nil {
return nil, err
}
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetMaxOpenConns(cfg.MaxOpenConns)
return db, nil
}
- 调用方负责传入配置,解耦硬编码
-
db.SetMaxIdleConns和db.SetMaxOpenConns必须设,否则默认无限开连接,压测时 DB 直接被拖垮 - 别在
init()里调这个函数——main 函数里显式调用,方便插桩、替换、延迟加载
事务里嵌套 DB.Create() 为啥不回滚
典型错误:在 Gin handler 里手动开事务,中间调用了一个封装好的 service 方法,那个方法内部又自己调了一次 DB.Create(),结果事务失效。
原因:GORM 的 *gorm.DB 是链式构建的,tx := DB.Begin() 返回的是新实例,所有操作必须基于 tx 而不是原始 DB。
- service 层方法必须接收
*gorm.DB参数,而不是用全局DB - handler 中:
tx := DB.Begin(); defer tx.Rollback(); err := someService.Create(tx, data) - service 中:
return tx.Create(&u).Error,不是DB.Create - 别信 “自动传播” —— GORM 没上下文透传,每个 DB 实例都是独立会话
Save() 和 Update() 对零值处理完全不同,FirstOrInit() 在 WHERE 条件命中时根本不走 CREATE 逻辑。这些细节不摸清,调试时就会花半天找“为什么数据没插入”。











