错误处理
error 接口
// error 是一个内置接口
type error interface {
Error() string
}
// 最简单的自定义错误
type MyError struct {
Code int
Message string
Cause error // 原始错误
}
func (e *MyError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("code=%d msg=%s cause=%s", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("code=%d msg=%s", e.Code, e.Message)
}
func (e *MyError) Unwrap() error {
return e.Cause
}
错误创建方式对比
| 方式 | 代码 | 性能 | 信息量 | 推荐场景 |
|---|
errors.New | errors.New("not found") | 最快 | 低 | 静态错误消息 |
fmt.Errorf | fmt.Errorf("user %s not found", id) | 快 | 中 | 格式化错误消息 |
fmt.Errorf + %w | fmt.Errorf("get user: %w", err) | 快 | 高 | 错误包装 |
| 自定义 struct | &MyError{Code: 404, ...} | 中 | 最高 | 需要结构化错误信息 |
errors.Join | errors.Join(err1, err2) | 快 | 中 | 合并多个错误(Go 1.20+) |
错误包装与解包(Go 1.13+)
// 包装错误
func getUser(id string) (*User, error) {
row := db.QueryRow("SELECT ... WHERE id = ?", id)
var u User
if err := row.Scan(&u.Name, &u.Email); err != nil {
return nil, fmt.Errorf("scan user %s: %w", id, err) // %w 包装
}
return &u, nil
}
// 调用方
user, err := getUser("123")
if err != nil {
// 日志记录完整错误链
log.Printf("failed: %+v", err)
// 判断是否是特定错误
var sqlErr *mysql.MySQLError
if errors.Is(err, sql.ErrNoRows) {
// 用户不存在
} else if errors.As(err, &sqlErr) {
// MySQL 特定错误
log.Printf("mysql error code: %d", sqlErr.Number)
}
return
}
errors.Is vs errors.As
| 函数 | 比较方式 | 用途 | 示例 |
|---|
errors.Is(err, target) | 值比较(沿 Unwrap 链) | 判断错误是否是特定值 | errors.Is(err, sql.ErrNoRows) |
errors.As(err, &target) | 类型比较(沿 Unwrap 链) | 提取特定类型的错误 | errors.As(err, &mysqlErr) |
// errors.Is:检查错误值
if errors.Is(err, context.DeadlineExceeded) {
// 超时错误
}
// errors.As:提取错误类型
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("path:", pathErr.Path)
}
// errors.Join:合并多个错误(Go 1.20+)
err := errors.Join(
errors.New("validation failed"),
errors.New("name is required"),
errors.New("email is invalid"),
)
// 输出:validation failed\nname is required\nemail is invalid
错误处理最佳实践
1. 哨兵错误(Sentinel Errors)
// 预定义错误变量
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
)
// 使用
func GetUser(id string) (*User, error) {
user, err := repo.FindByID(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound // 转换为业务错误
}
return nil, fmt.Errorf("find user: %w", err)
}
return user, nil
}
// 调用方判断
user, err := GetUser("123")
if errors.Is(err, ErrUserNotFound) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
2. 自定义错误类型
// 带业务码的错误
type AppError struct {
Code string // 业务错误码,如 "USER_NOT_FOUND"
Message string // 用户友好的消息
Err error // 原始错误
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 构造函数
func NewAppError(code, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
// 使用
func GetUser(id string) (*User, error) {
user, err := repo.FindByID(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, NewAppError("USER_NOT_FOUND", "用户不存在", err)
}
return nil, NewAppError("DB_ERROR", "数据库异常", err)
}
return user, nil
}
// 调用方
user, err := GetUser("123")
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.Code {
case "USER_NOT_FOUND":
http.Error(w, appErr.Message, http.StatusNotFound)
case "DB_ERROR":
http.Error(w, appErr.Message, http.StatusInternalServerError)
}
return
}
// 未知错误
http.Error(w, "internal error", http.StatusInternalServerError)
}
3. 错误处理分层
Repository 层:返回数据库原始错误
↓ fmt.Errorf 包装
Service 层:转换为业务错误 + 上下文
↓ errors.As 判断
Handler 层:转换为 HTTP 状态码 + 用户友好消息
// Repository 层
func (r *UserRepo) FindByID(ctx context.Context, id string) (*entity.User, error) {
var u entity.User
err := r.db.WithContext(ctx).First(&u, "id = ?", id).Error
if err != nil {
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return &u, nil
}
// Service 层
func (s *UserService) GetByID(ctx context.Context, id string) (*dto.UserDTO, error) {
entity, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return dto.FromEntity(entity), nil
}
// Handler 层
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := h.svc.GetByID(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
response.JSON(w, http.StatusNotFound, map[string]string{
"error": "用户不存在",
})
default:
log.Printf("get user failed: %v", err)
response.JSON(w, http.StatusInternalServerError, map[string]string{
"error": "内部错误",
})
}
return
}
response.JSON(w, http.StatusOK, user)
}
panic 与 recover
// panic:程序遇到不可恢复的错误
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
// recover:捕获 panic,防止程序崩溃
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return divide(a, b), nil
}
何时用 panic / 何时用 error
| 场景 | 用 error | 用 panic |
|---|
| 可预期的业务错误 | ✓ | |
| 用户输入错误 | ✓ | |
| 网络超时 | ✓ | |
| 文件不存在 | ✓ | |
| 数组越界 | | ✓(编程错误) |
| nil 指针解引用 | | ✓(编程错误) |
| 初始化失败 | | ✓(程序无法继续运行) |
| 不可能到达的分支 | | ✓(default 分支防御) |
panic 在 HTTP 中的恢复
// Recovery 中间件
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
错误处理检查清单
| 检查项 | 说明 |
|---|
| ✅ 不要忽略错误 | _ = doSomething() 要有明确理由 |
| ✅ 错误要包装上下文 | fmt.Errorf("doing X: %w", err) |
✅ 哨兵错误用 errors.Is 判断 | 不要用 == 或字符串匹配 |
✅ 自定义错误用 errors.As 提取 | 沿 Unwrap 链查找 |
| ✅ error 只返回一次 | 在合适的层级返回,不要层层传递原始错误 |
| ✅ 日志记录和错误返回不要重复 | 记录 OR 返回,不要既记录又返回 |
| ✅ 面向用户的错误消息要友好 | ”用户不存在”而非 “record not found” |
| ✅ 不要用 panic 做正常的错误处理 | panic 只用于不可恢复的错误 |