错误处理规范
核心原则
错误处理必须明确清晰,使用错误包装保留错误链,确保错误可追踪和调试。
允许的做法
错误定义
- 使用 errors.New 定义固定错误
- 使用 fmt.Errorf 创建动态错误
- 使用 %w 包装错误
go
// ✅ 正确 - 定义错误
var (
ErrUserNotFound = errors.New("用户不存在")
ErrEmailExists = errors.New("邮箱已存在")
ErrInvalidPassword = errors.New("密码错误")
)
// ✅ 正确 - 创建错误
if user == nil {
return fmt.Errorf("用户 ID %d 不存在", userID)
}
// ✅ 正确 - 包装错误
if err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}错误判断
- 使用 errors.Is 判断错误类型
- 使用 errors.As 转换错误类型
go
// ✅ 正确 - 错误判断
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("记录不存在")
}
return fmt.Errorf("查询失败: %w", err)
}
// ✅ 正确 - 错误转换
var validationErr *validator.ValidationErrors
if errors.As(err, &validationErr) {
// 处理校验错误
}Service 层错误处理
- 返回 明确的错误
- 包装 底层错误
go
// ✅ 正确 - Service 错误处理
func (s *UserService) Create(ctx context.Context, req *CreateUserRequest) (*models.User, error) {
// 业务校验
existingUser, err := s.findByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询邮箱失败: %w", err)
}
if existingUser != nil {
return nil, errors.New("邮箱已存在")
}
// 创建用户
user := &models.User{
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now().Unix(),
}
if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
return user, nil
}Handler 层错误处理
- 转换 错误为 HTTP 响应
- 记录 错误日志
go
// ✅ 正确 - Handler 错误处理
func (h *UserHandler) Create(c echo.Context) error {
user, err := h.userService.Create(c.Request().Context(), req)
if err != nil {
logger.Errorw("创建用户失败",
"error", err,
"email", req.Email,
)
// 根据错误类型返回不同响应
if err.Error() == "邮箱已存在" {
return c.JSON(400, response.Fail(3700, "邮箱已存在"))
}
return c.JSON(500, response.Fail(500, "创建用户失败"))
}
return c.JSON(201, response.Success(user))
}日志记录
- 使用 结构化日志
- 记录 上下文信息
go
// ✅ 正确 - 结构化日志
import "code.hellotalk.com/infra/logger"
// 普通日志
logger.Infow("用户创建成功", "userID", user.ID, "email", user.Email)
// 错误日志
logger.Errorw("创建用户失败",
"error", err,
"email", req.Email,
"requestID", requestID,
)
// 调试日志
logger.Debugw("处理请求", "requestID", requestID)禁止的做法
- 禁止 忽略错误
- 禁止 使用 panic(除非不可恢复)
- 避免 打印错误后继续执行
go
// ❌ 错误
user, _ := GetUser(id) // 忽略错误
panic("error") // 避免 panic
if err != nil {
fmt.Println(err) // 只打印不处理
}
// ✅ 正确
user, err := GetUser(id)
if err != nil {
return fmt.Errorf("查询用户失败: %w", err)
}特殊场景
事务错误处理
go
// ✅ 正确 - 事务错误处理
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}
if err := tx.Create(&profile).Error; err != nil {
return fmt.Errorf("创建资料失败: %w", err)
}
return nil
})
if err != nil {
logger.Errorw("事务执行失败", "error", err)
return fmt.Errorf("操作失败: %w", err)
}参数校验错误
go
// ✅ 正确 - 参数校验错误处理
if err := c.Validate(req); err != nil {
logger.Warnw("参数校验失败", "error", err)
return c.JSON(400, response.Fail(1001, "参数格式错误"))
}