5月28日 01:12

GORM 中如何处理错误?

GORM 的所有数据库操作都可能返回错误,正确处理这些错误是写出健壮 Go 应用的基本功。GORM 的错误处理方式和普通 Go 代码略有不同——因为它采用了链式调用 API,错误不会直接从方法返回,而是存储在 *gorm.DBError 字段中。

错误处理基础

检查 db.Error

GORM 在执行 Finisher 方法(CreateFirstFindUpdateDelete 等)后会将错误写入 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 操作都遵循同样的模式:

go
if 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. 记录未找到

FirstLastTake 找不到记录时会返回 gorm.ErrRecordNotFound,用 errors.Is 判断:

go
var 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 }

FindScan 查询空结果集时不会返回 ErrRecordNotFound,只会返回空切片,这点需要特别注意。

2. 连接错误

数据库连接失败或中断属于基础设施问题:

go
db, 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 自动转换:

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ TranslateError: true, // 关键配置 })

启用后,数据库返回的原始错误会被转换为 GORM 标准错误:

go
if 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 的钩子在写入前校验数据:

go
func (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. 事务中的错误处理

事务中任何一步返回错误都会自动回滚:

go
err := 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 恢复:

go
tx := 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. 自定义错误类型

把数据库错误转换为业务错误,上层代码不需要关心底层细节:

go
type 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 提取具体错误:

go
err := 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 内置了日志系统,按级别控制输出:

go
newLogger := 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. 重试机制

网络抖动导致的临时错误适合重试:

go
func 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 会把不同数据库驱动的原始错误统一转换为标准错误类型:

go
db, 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 的惯用写法:

go
user, 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 提取错误类型,配合自定义错误体系使用:

go
err := 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) }

生产环境错误处理策略

中间件模式

封装一个数据访问层,统一处理错误转换和日志:

go
type 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 兜底:

go
func 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) }

监控告警

生产环境需要把错误接入监控:

go
func 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 代替字符串匹配,把数据库错误封装成业务错误再向上传递。这样做既能让代码可维护,也能在生产环境中快速定位问题。

标签:Gorm