This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/14-错误处理.md
T
2026-04-28 20:23:33 +08:00

8.3 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
错误处理
ErrRecordNotFound
ErrDuplicatedKey
TransactionFinished
2026-04-28 00:00

错误处理

概述

GORM 从不 panic——所有错误都通过 .Error 字段返回。这种设计让业务层可以在不中断程序的情况下优雅地处理异常,但也意味着开发者需要养成「始终检查 Error」的习惯。

flowchart TD
    Result["db.XXX() → result"] --> HasErr{"result.Error != nil?"}
    HasErr --> |否| Success["继续业务逻辑 ✓"]
    HasErr --> |是| CheckType{判断错误类型}
    
    CheckType --> |ErrRecordNotFound| NotFound["404 / 默认值"]
    CheckType --> |ErrDuplicatedKey| Duplicate["409 / 提示用户修改"]
    CheckType --> |TransactionFinished| TxErr["500 / 事务已被提交或回滚"]
    CheckType --> |其他 errors.Is| Generic["记录日志 / 返回通用错误"]
    
    style Start fill:#4FC08D,color:#fff
    style Success fill:#3B82F6,color:#fff
    style NotFound fill:#EF4444,color:#fff

核心错误常量

gorm.ErrRecordNotFound

var user User
result := db.First(&user, 1)

if errors.Is(result.Error, gorm.ErrRecordNotFound) {
    // 记录不存在 —— 可以返回 404 或设置默认值
    return http.NotFound(w, r)
} else if result.Error != nil {
    // 真正的数据库错误
    log.Printf("查询用户失败: %v", result.Error)
    return err
}

// 成功分支
log.Printf("找到用户: %s", user.Name)

[!tip] 为什么用 errors.Is 而不是 ==? gorm.ErrRecordNotFound 是一个 sentinal error(哨兵错误),Go 标准库的 errors.Is() 能正确处理包装过的错误链。直接用 == 在复杂场景中可能失效。

gorm.ErrDuplicatedKey / gorm.ErrForeignKeyConstraintViolated

result := db.Create(&User{Email: "alice@example.com"})

if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
    // 唯一约束冲突 —— 通常是 Email 已注册
    return fmt.Errorf("该邮箱已被注册")
}

// 外键约束失败
type Order struct { UserID uint }
result := db.Create(&Order{UserID: 999})
if errors.Is(result.Error, gorm.ErrForeignKeyConstraintViolated) {
    return fmt.Errorf("指定的用户不存在")
}

gorm.TransactionFinished

tx := db.Begin()
tx.Commit() // 先提交
tx.Rollback() // ❌ 再回滚会报错!
// result.Error == gorm.ErrTransactionFinished

[!warning] 常见触发场景 这个错误最常见于 defer Rollback() 模式——如果 Commit 成功了但 defer 仍会执行 Rollback:

func Example() error {
    tx := db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
            panic(r)
        }
    }()
    
    // ... 操作 ...
    
    if err := tx.Commit().Error; err != nil {
        tx.Rollback()
        return err
    }
    // 到达这里说明 Commit 成功 —— defer 不会进 panic 分支
    return nil
}

常用错误常量汇总

常量 含义 典型 HTTP 状态码 处理建议
gorm.ErrRecordNotFound 查询结果为空 404 返回友好提示或默认值
gorm.ErrDuplicatedKey 唯一约束冲突 409 提示用户修改输入
gorm.ErrForeignKeyConstraintViolated 外键约束失败 400 检查关联数据是否存在
gorm.ErrInvalidData 数据无效 400 校验输入后重试
gorm.ErrTransactionFinished 事务已提交/回滚 500 代码逻辑 BUG,修复代码
gorm.ErrUnsupportedDriver 不支持的驱动 配置错误 检查 dsn 和驱动导入
gorm.ErrInvalidTransaction 无效的事务操作 500 检查事务上下文是否正确

自定义错误判断

包装业务错误

func GetUserByID(db *gorm.DB, id uint) (*User, error) {
    var user User
    if err := db.First(&user, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("用户 ID=%d 不存在: %w", id, err)
        }
        return nil, fmt.Errorf("查询用户失败: %w", err)
    }
    return &user, nil
}

自定义错误处理器

// 统一错误解析器,适合中间件或框架层使用
func ParseDBError(err error) (int, string) {
    switch {
    case errors.Is(err, gorm.ErrRecordNotFound):
        return http.StatusNotFound, "资源未找到"
    case errors.Is(err, gorm.ErrDuplicatedKey):
        return http.StatusConflict, "数据冲突,请检查输入"
    case errors.Is(err, gorm.ErrForeignKeyConstraintViolated):
        return http.StatusBadRequest, "关联数据无效"
    case errors.Is(err, gorm.ErrTransactionFinished):
        return http.StatusInternalServerError, "系统内部错误"
    default:
        log.Printf("未分类数据库错误: %v", err)
        return http.StatusInternalServerError, "数据库操作失败"
    }
}

// 在 Gin handler 中使用
func HandleCreateUser(c *gin.Context) {
    err := createUserService(c.Request)
    if err != nil {
        status, msg := ParseDBError(err)
        c.JSON(status, gin.H{"error": msg})
        return
    }
    c.JSON(201, gin.H{"message": "创建成功"})
}

错误检查的正确姿势

❌ 错误的写法

// 1. 完全忽略错误
db.Create(&user)

// 2. 只检查非 nil(会误判 ErrRecordNotFound)
if err := db.First(&user, 1).Error; err != nil {
    // ErrRecordNotFound 也会被当成一般错误处理
    log.Printf("error: %v", err)
}

// 3. 先判断 Error != nil 再用 == 比较
err := db.First(&user, 1).Error
if err != nil && err == gorm.ErrRecordNotFound {
    // 在某些情况下可能漏判
}

✅ 正确的写法

// 模式一:errors.Is 优先
if err := db.First(&user, 1).Error; err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
        // 单独处理
    } else {
        // 其他数据库错误
    }
}

// 模式二:先赋值再判断
result := db.First(&user, 1)
switch {
case errors.Is(result.Error, gorm.ErrRecordNotFound):
    handleNotFound()
case result.Error != nil:
    handleError(result.Error)
default:
    handleSuccess(user)
}

// 模式三:简洁的单路径
if result := db.First(&user, 1); result.Error != nil {
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        return nil, fmt.Errorf("user not found")
    }
    return nil, fmt.Errorf("db error: %w", result.Error)
}

连接错误与超时处理

// 检测数据库是否可达
sqlDB, err := db.DB()
if err != nil {
    return err
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := sqlDB.PingContext(ctx); err != nil {
    log.Printf("数据库连接异常: %v", err)
    // 可以尝试重连、切换从库等
    return fmt.Errorf("database unavailable")
}

[!tip] 结合健康检查 在生产环境中,应该定期执行 PingContext 作为 K8s liveness/readiness probe,确保服务知道数据库是否可用。

错误处理决策流程图

flowchart TD
    Start[操作返回结果] --> CheckErr{"result.Error 非空?"}
    CheckErr --> |否| Success["操作成功"]
    CheckErr --> |是| FirstCheck{errors.Is...}
    
    FirstCheck --> |ErrRecordNotFound| Handler1["处理: 返回默认值/404"]
    FirstCheck --> |ErrDuplicatedKey| Handler2["处理: 提示重复"]
    FirstCheck --> |ErrForeignKeyConstraintViolated| Handler3["处理: 检查关联"]
    FirstCheck --> |ErrTransactionFinished| Handler4["处理: 代码缺陷修复"]
    FirstCheck --> |都不匹配| DefaultHandler["处理: 通用错误/500"]
    
    style Start fill:#4FC08D,color:#fff
    style Success fill:#3B82F6,color:#fff
    style DefaultHandler fill:#EF4444,color:#fff

常见坑点速查

问题 原因 解决方案
忘记检查 .Error 以为方法直接返回 error 养成 if err := xxx().Error; err != nil 习惯
用 == 而非 errors.Is 哨兵错误被 fmt.Errorf("%w") 包装后丢失 统一使用 errors.Is(err, gorm.xxx)
Commit 后再 Rollback defer 无条件执行回滚 只在 panic 分支中 Rollback
把所有错误当 RecordNotFound 没做错误类型分层 先用 errors.Is 精确判断
数据库断开时没检测到 连接池复用旧连接 PingContext 定期探测 + SetConnMaxLifetime

关联笔记