GORM 提供了完善的错误处理机制,正确处理错误对于构建稳定的应用程序至关重要。
错误处理基础
检查错误
GORM 的所有操作都会返回错误,需要检查 db.Error:
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. 记录未找到错误
govar user User result := db.First(&user, 999) if errors.Is(result.Error, gorm.ErrRecordNotFound) { log.Println("用户不存在") // 处理记录未找到的情况 } else if result.Error != nil { log.Printf("查询失败: %v", result.Error) return result.Error }
2. 连接错误
godb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Printf("数据库连接失败: %v", err) panic(err) } // 测试连接 sqlDB, err := db.DB() if err != nil { log.Printf("获取数据库连接失败: %v", err) panic(err) } if err := sqlDB.Ping(); err != nil { log.Printf("数据库 ping 失败: %v", err) panic(err) }
3. 约束错误
go// 唯一约束冲突 user := User{Email: "existing@example.com"} if err := db.Create(&user).Error; err != nil { if strings.Contains(err.Error(), "Duplicate entry") { log.Println("邮箱已存在") return errors.New("邮箱已存在") } return err } // 外键约束错误 if err := db.Create(&order).Error; err != nil { if strings.Contains(err.Error(), "foreign key constraint") { log.Println("用户不存在") return errors.New("用户不存在") } return err }
4. 验证错误
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 } // 创建时处理验证错误 user := User{Name: "", Email: "invalid"} if err := db.Create(&user).Error; err != nil { log.Printf("验证失败: %v", err) return err }
错误处理最佳实践
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 }
2. 自定义错误处理
gotype DBError struct { Code int Message string Err error } func (e *DBError) Error() string { return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err) } func HandleDBError(err error) error { if err == nil { return nil } if errors.Is(err, gorm.ErrRecordNotFound) { return &DBError{Code: 404, Message: "记录未找到", Err: err} } if strings.Contains(err.Error(), "Duplicate entry") { return &DBError{Code: 409, Message: "记录已存在", Err: err} } if strings.Contains(err.Error(), "foreign key constraint") { return &DBError{Code: 400, Message: "关联记录不存在", Err: err} } return &DBError{Code: 500, Message: "数据库错误", Err: err} } // 使用 if err := db.Create(&user).Error; err != nil { return HandleDBError(err) }
3. 错误日志记录
go// 配置日志 import "gorm.io/gorm/logger" newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Second, LogLevel: logger.Error, IgnoreRecordNotFoundError: true, Colorful: true, }, ) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: newLogger, }) // 自定义错误处理 func logError(operation string, err error) { if err != nil { log.Printf("%s 失败: %v", operation, err) // 可以发送到监控系统 // metrics.ErrorCounter.Inc() } } // 使用 if err := db.Create(&user).Error; err != nil { logError("创建用户", err) }
4. 重试机制
gofunc withRetry(maxRetries int, fn func() error) error { var lastErr error for i := 0; i < maxRetries; i++ { if err := fn(); err != nil { lastErr = err // 如果是连接错误,可以重试 if isConnectionError(err) { time.Sleep(time.Second * time.Duration(i+1)) continue } // 其他错误不重试 return err } return nil } return fmt.Errorf("重试 %d 次后仍然失败: %v", maxRetries, lastErr) } func isConnectionError(err error) bool { return strings.Contains(err.Error(), "connection") || strings.Contains(err.Error(), "timeout") } // 使用 err := withRetry(3, func() error { return db.Create(&user).Error })
错误恢复
使用 recover 处理 panic
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 recovered: %v", r) } }() return fn(db) } // 使用 err := safeDBOperation(db, "创建用户", func(db *gorm.DB) error { return db.Create(&user).Error })
错误处理中间件
创建错误处理中间件
gotype DBHandler struct { db *gorm.DB } func (h *DBHandler) HandleError(err error) error { if err == nil { return nil } if errors.Is(err, gorm.ErrRecordNotFound) { return &AppError{ Code: http.StatusNotFound, Message: "资源不存在", Err: err, } } return &AppError{ Code: http.StatusInternalServerError, Message: "数据库操作失败", Err: err, } } func (h *DBHandler) CreateUser(user *User) error { if err := h.db.Create(user).Error; err != nil { return h.HandleError(err) } return nil }
注意事项
- 始终检查错误:不要忽略任何数据库操作的错误
- 区分错误类型:根据不同的错误类型采取不同的处理策略
- 提供有意义的错误信息:错误信息应该清晰、具体
- 记录错误日志:记录详细的错误信息便于调试
- 避免暴露敏感信息:不要将数据库错误直接暴露给用户
- 使用事务:对于多个操作,使用事务确保数据一致性
- 重试机制:对于临时性错误,可以实现重试机制
- 监控告警:设置错误监控和告警机制
常见问题
Q: 如何区分记录未找到和其他错误?
A: 使用 errors.Is(err, gorm.ErrRecordNotFound) 来判断是否是记录未找到错误。
Q: 错误信息应该记录到日志还是返回给用户?
A: 详细错误信息应该记录到日志,返回给用户的应该是简化的、友好的错误信息。
Q: 如何处理数据库连接断开的情况?
A: 实现重试机制,或者使用连接池自动重连功能。
Q: 事务中的错误如何处理?
A: 在事务回调中返回错误会自动回滚事务,不需要手动处理。