GORM 中如何处理错误?
GORM 的所有数据库操作都可能返回错误,正确处理这些错误是写出健壮 Go 应用的基本功。GORM 的错误处理方式和普通 Go 代码略有不同——因为它采用了链式调用 API,错误不会直接从方法返回,而是存储在 *gorm.DB 的 Error 字段中。
错误处理基础
检查 db.Error
GORM 在执行 Finisher 方法(Create、First、Find、Update、Delete 等)后会将错误写入 db.Error。检查方式有两种:
go// 方式一:直接调用 .Error if err := db.Create(&user).Error; err != nil { return err } // 方式二:先获取结果再检查 result := db.First(&user, 1) if result.Error != nil { return result.Error }
result 还包含 RowsAffected 等信息,需要时可选用方式二。
所有 CRUD 操作都遵循同样的模式:
goif err := db.Create(&user).Error; err != nil { log.Printf("创建失败: %v", err) return err } if err := db.First(&user, 1).Error; err != nil { log.Printf("查询失败: %v", err) return err } if err := db.Model(&user).Update("name", "John").Error; err != nil { log.Printf("更新失败: %v", err) return err } if err := db.Delete(&user).Error; err != nil { log.Printf("删除失败: %v", err) return err }
常见错误类型
1. 记录未找到
First、Last、Take 找不到记录时会返回 gorm.ErrRecordNotFound,用 errors.Is 判断:
govar user User result := db.First(&user, 999) if errors.Is(result.Error, gorm.ErrRecordNotFound) { // 记录不存在,不是程序错误,可能只是业务逻辑 return nil, ErrUserNotFound } else if result.Error != nil { // 其他数据库错误 return nil, result.Error }
Find 和 Scan 查询空结果集时不会返回 ErrRecordNotFound,只会返回空切片,这点需要特别注意。
2. 连接错误
数据库连接失败或中断属于基础设施问题:
godb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalf("数据库连接失败: %v", err) } sqlDB, err := db.DB() if err != nil { log.Fatalf("获取底层连接失败: %v", err) } // 配置连接池 sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) // 健康检查 if err := sqlDB.Ping(); err != nil { log.Printf("数据库不可达: %v", err) }
连接池配置直接影响错误发生的频率——MaxOpenConns 过小会导致等待超时,ConnMaxLifetime 过长会导致 MySQL 主动断开连接。
3. 约束错误
唯一约束冲突、外键约束违反是高频错误。不要用字符串匹配来判断,启用 TranslateError 让 GORM 自动转换:
godb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ TranslateError: true, // 关键配置 })
启用后,数据库返回的原始错误会被转换为 GORM 标准错误:
goif err := db.Create(&user).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return ErrEmailAlreadyExists } return err } if err := db.Create(&order).Error; err != nil { if errors.Is(err, gorm.ErrForeignKeyViolated) { return ErrUserNotFound } return err }
不启用 TranslateError 时,你只能拿到 MySQL 返回的原始错误字符串,用 strings.Contains 做匹配——这种方式脆弱且不可靠,换了数据库驱动就可能失效。
4. 验证错误
利用 GORM 的钩子在写入前校验数据:
gofunc (u *User) BeforeCreate(tx *gorm.DB) error { if u.Name == "" { return errors.New("用户名不能为空") } if !strings.Contains(u.Email, "@") { return errors.New("邮箱格式不正确") } return nil }
钩子返回错误时,当前操作会被中止,db.Error 会拿到钩子返回的错误。
错误处理最佳实践
1. 事务中的错误处理
事务中任何一步返回错误都会自动回滚:
goerr := db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&user).Error; err != nil { return err // 自动回滚 } if err := tx.Create(&profile).Error; err != nil { return err // 自动回滚 } return nil // 自动提交 }) if err != nil { log.Printf("事务失败: %v", err) return err }
手动控制事务时需要处理 panic 恢复:
gotx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() log.Printf("事务 panic: %v", r) } }() if err := tx.Create(&user).Error; err != nil { tx.Rollback() return err } if err := tx.Commit().Error; err != nil { return err }
2. 自定义错误类型
把数据库错误转换为业务错误,上层代码不需要关心底层细节:
gotype AppError struct { Code int Message string Err error } func (e *AppError) Error() string { return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err) } func WrapDBError(err error) error { if err == nil { return nil } switch { case errors.Is(err, gorm.ErrRecordNotFound): return &AppError{Code: 404, Message: "资源不存在", Err: err} case errors.Is(err, gorm.ErrDuplicatedKey): return &AppError{Code: 409, Message: "记录已存在", Err: err} case errors.Is(err, gorm.ErrForeignKeyViolated): return &AppError{Code: 400, Message: "关联记录不存在", Err: err} default: return &AppError{Code: 500, Message: "数据库操作失败", Err: err} } }
上层用 errors.As 提取具体错误:
goerr := db.Create(&user).Error if err != nil { wrapped := WrapDBError(err) var appErr *AppError if errors.As(wrapped, &appErr) { // 根据 appErr.Code 返回不同 HTTP 状态码 } return wrapped }
3. 日志配置
GORM 内置了日志系统,按级别控制输出:
gonewLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: 200 * time.Millisecond, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, // 生产环境忽略 NotFound Colorful: false, }, ) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: newLogger, })
IgnoreRecordNotFoundError 设为 true 可以避免 ErrRecordNotFound 刷屏,因为记录不存在往往是正常的业务场景。
4. 重试机制
网络抖动导致的临时错误适合重试:
gofunc withRetry(db *gorm.DB, maxRetries int, fn func(*gorm.DB) error) error { var lastErr error for i := 0; i < maxRetries; i++ { if err := fn(db); err != nil { lastErr = err if isTransientError(err) { time.Sleep(time.Second * time.Duration(i+1)) continue } return err } return nil } return fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr) } func isTransientError(err error) bool { return errors.Is(err, driver.ErrBadConn) || strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "timeout") }
注意:只有连接级别的临时错误才值得重试,约束冲突、记录不存在这类错误重试毫无意义。
GORM v2 错误处理进阶
TranslateError 配置
这是 GORM v2 推荐的错误处理方式。开启后 GORM 会把不同数据库驱动的原始错误统一转换为标准错误类型:
godb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ TranslateError: true, }) // 然后就可以统一用 errors.Is 判断 if errors.Is(err, gorm.ErrDuplicatedKey) { // 唯一键冲突 } if errors.Is(err, gorm.ErrForeignKeyViolated) { // 外键约束失败 }
不开启 TranslateError,你只能拿到类似 Error 1062: Duplicate entry 'xxx' for key 'email' 的原始字符串。
泛型 API 错误处理
GORM v2 提供了泛型 API,错误直接从方法返回,符合 Go 的惯用写法:
gouser, err := gorm.G[User](db).Where("name = ?", "jinzhu").First(ctx) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err }
泛型 API 不需要检查 db.Error,错误处理和普通 Go 代码一样直观。
errors.Is 和 errors.As 组合使用
errors.Is 判断错误值,errors.As 提取错误类型,配合自定义错误体系使用:
goerr := db.Create(&user).Error if err != nil { // 先用 Is 判断已知错误 if errors.Is(err, gorm.ErrDuplicatedKey) { return ErrDuplicate } // 再用 As 提取自定义错误 var appErr *AppError if errors.As(err, &appErr) { return appErr } // 兜底 return fmt.Errorf("创建用户失败: %w", err) }
生产环境错误处理策略
中间件模式
封装一个数据访问层,统一处理错误转换和日志:
gotype UserRepository struct { db *gorm.DB } func (r *UserRepository) Create(user *User) error { if err := r.db.Create(user).Error; err != nil { wrapped := WrapDBError(err) log.Printf("创建用户失败: %v", wrapped) return wrapped } return nil } func (r *UserRepository) FindByID(id uint) (*User, error) { var user User if err := r.db.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil // 业务上返回 nil 表示不存在 } return nil, WrapDBError(err) } return &user, nil }
把 db 操作收敛到 Repository 中,避免业务代码直接处理数据库错误细节。
Panic 恢复
数据库操作中可能触发 panic(比如 nil 指针),用 defer recover 兜底:
gofunc safeDBOperation(db *gorm.DB, operation string, fn func(*gorm.DB) error) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("%s 发生 panic: %v", operation, r) log.Printf("数据库操作 panic: %v", r) } }() return fn(db) }
监控告警
生产环境需要把错误接入监控:
gofunc monitoredOperation(operation string, fn func() error) error { err := fn() if err != nil { // 记录指标 metrics.DBErrors.WithLabelValues(operation, fmt.Sprintf("%T", err)).Inc() // 触发告警 if isCriticalError(err) { alert.Send("数据库严重错误", err.Error()) } } return err }
把错误区分等级:ErrRecordNotFound 不告警,连接超时发 warning,连接池耗尽发 critical——这样才能在大量错误中抓住真正需要处理的问题。
掌握 GORM 错误处理的关键在于三点:开启 TranslateError 统一错误类型,用 errors.Is/errors.As 代替字符串匹配,把数据库错误封装成业务错误再向上传递。这样做既能让代码可维护,也能在生产环境中快速定位问题。