悠悠楠杉
优雅处理Golang数据库操作错误:从sql.ErrNoRows到错误处理范式
一、为什么需要专门处理数据库错误
在Web服务开发中,数据库操作错误处理不当会导致一系列连锁反应。我们来看个真实案例:某金融系统因未正确处理sql.ErrNoRows
,导致空查询结果被当作系统异常,触发不必要的告警。这种错误处理方式暴露了三个典型问题:
- 错误信息模糊:原始错误直接暴露给调用方
- 处理逻辑重复:每个DAO方法都在重复判断相同错误
- 上下文缺失:无法追溯错误发生的业务场景
go
// 典型的问题代码示例
err := db.QueryRow("SELECT...").Scan(&data)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("not found")
}
return nil, err
}
二、特定数据库错误处理技巧
2.1 sql.ErrNoRows的本质
这个看似简单的错误实际包含多层含义:
- 可能是正常的业务空状态(如用户未配置信息)
- 也可能是真正的数据异常(按ID查询不存在的记录)
推荐处理方式:go
func (r UserRepo) GetByID(ctx context.Context, id int64) (User, error) {
const query = SELECT...
var user User
err := r.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrUserNotFound.WithMeta("user_id", id)
case err != nil:
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
}
2.2 其他需要特别处理的错误
| 错误类型 | 判断方法 | 处理建议 |
|-----------------------|--------------------------|--------------------------|
| 连接超时 | errors.Is(err, context.DeadlineExceeded)
| 添加重试机制 |
| 唯一约束冲突 | 检查错误字符串或数据库错误码 | 转换为业务冲突错误类型 |
| 事务冲突 | 判断sql.ErrTxDone
| 区分重试与业务回滚 |
三、构建统一的错误处理体系
3.1 错误封装的三层结构
原始层:保留数据库驱动原始错误
go // 使用%w保留错误链 if err := row.Scan(...); err != nil { return fmt.Errorf("scan order data: %w", err) }
业务语义层:定义领域错误类型
go var ( ErrOrderNotFound = errors.NewClass("order not found") ErrOrderConflict = errors.NewClass("order conflict") )
元信息层:附加上下文数据
go return ErrOrderNotFound. WithMeta("order_id", orderID). WithCause(err)
3.2 DAO层的错误处理模板
go
func (r *OrderRepository) UpdateOrderStatus(ctx context.Context, orderID string, status int) error {
const query = UPDATE orders SET status=$1 WHERE id=$2
result, err := r.db.ExecContext(ctx, query, status, orderID)
if err != nil {
if isDuplicateError(err) { // 自定义判断函数
return ErrOrderConflict.WithCause(err)
}
return fmt.Errorf("update order status: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return ErrOrderNotFound.WithMeta("order_id", orderID)
}
return nil
}
四、进阶实践:错误处理中间件
对于Web服务,可以在HTTP层统一转换错误:
go
func ErrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
if err := GetRequestError(r); err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
WriteJSONError(w, http.StatusNotFound, err)
case errors.Is(err, ErrOrderConflict):
WriteJSONError(w, http.StatusConflict, err)
default:
WriteJSONError(w, http.StatusInternalServerError,
errors.New("internal server error"))
}
}
})
}
五、错误处理的质量标准
评估错误处理是否到位的checklist:
1. 是否保留了完整的错误链(errors.Is()
可追溯)
2. 敏感信息是否已过滤(如SQL语句)
3. 日志是否包含足够排查信息
4. 相同错误是否在多层重复处理
5. 错误类型是否具有明确的业务语义
最终建议:在项目初期就建立《错误处理规范》,约定错误分类、封装方式和日志格式,这将大幅降低后期的维护成本。
作者经验谈:在实际项目中,我们通过统一错误处理方案将数据库相关Bug减少了70%。关键点在于把错误处理视为重要的业务逻辑,而非简单的技术细节。