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

272 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [GORM, Go, ORM, 错误处理, ErrRecordNotFound, ErrDuplicatedKey, TransactionFinished]
create time: 2026-04-28 00:00
---
# 错误处理
## 概述
GORM **从不 panic**——所有错误都通过 `.Error` 字段返回。这种设计让业务层可以在不中断程序的情况下优雅地处理异常,但也意味着开发者需要养成「始终检查 Error」的习惯。
```mermaid
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
```go
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
```go
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
```go
tx := db.Begin()
tx.Commit() // 先提交
tx.Rollback() // ❌ 再回滚会报错!
// result.Error == gorm.ErrTransactionFinished
```
> [!warning] 常见触发场景
> 这个错误最常见于 `defer Rollback()` 模式——如果 Commit 成功了但 defer 仍会执行 Rollback:
> ```go
> 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 | 检查事务上下文是否正确 |
## 自定义错误判断
### 包装业务错误
```go
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
}
```
### 自定义错误处理器
```go
// 统一错误解析器,适合中间件或框架层使用
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": "创建成功"})
}
```
## 错误检查的正确姿势
### ❌ 错误的写法
```go
// 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 {
// 在某些情况下可能漏判
}
```
### ✅ 正确的写法
```go
// 模式一: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)
}
```
## 连接错误与超时处理
```go
// 检测数据库是否可达
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,确保服务知道数据库是否可用。
## 错误处理决策流程图
```mermaid
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 |
## 关联笔记
- [[03-CRUD 操作]]
- [[08-事务管理]]
- [[16-日志与调试]]