17370845950

Golang如何统一项目中的错误定义
不能只用 errors.New 或 fmt.Errorf,因其导致错误信息分散、无法区分业务/系统错误、下游难判断类型或映射错误码、日志缺上下文;应定义全局错误变量并使用自定义 Error 类型统一管理。

为什么不能只用 errors.Newfmt.Errorf

直接用 errors.New("user not found")fmt.Errorf("failed to parse config: %w", err) 看似简单,但会导致几个实际问题:错误信息散落在各处、无法区分业务错误和系统错误、下游难以做类型判断或错误码映射、日志中缺少上下文字段(如请求 ID、用户 ID)。尤其当项目接入监控或需要国际化时,这种写法会让错误处理迅速失控。

定义全局错误变量 + 自定义错误类型

推荐在 pkg/errorsinternal/errors 包中集中声明错误变量,并搭配自定义结构体承载错误码、HTTP 状态码、可序列化字段。关键不是“造轮子”,而是让错误具备可识别性与可扩展性。

package errors

import "fmt"

type Code int

const (
    ErrUserNotFound Code = 1001
    ErrInvalidToken Code = 1002
    ErrDatabase     Code = 5001
)

type Error struct {
    Code    Code
    Message string
    Status  int // HTTP status, e.g. 404, 401, 500
}

func (e *Error) Error() string {
    return e.Message
}

func (e *Error) ErrorCode() Code {
    return e.Code
}

var (
    UserNotFound = &Error{Code: ErrUserNotFound, Message: "user not found", Status: 404}
    InvalidToken = &Error{Code: ErrInvalidToken, Message: "invalid auth token", Status: 401}
)
  • 所有业务错误都从这里导出变量,避免拼写错误和重复定义
  • *Error 类型可被 errors.Iserrors.As 正确识别,支持错误链嵌套
  • 如果需要携带额外字段(如 UserID),可在结构体中添加并实现 Unwrap() 方法

如何包装底层错误而不丢失业务语义

调用数据库、HTTP 客户端等可能返回原始错误时,不能简单用 fmt.Errorf("%w", err) —— 这会丢掉错误码和状态码。必须用自定义构造函数做“语义升格”。

func GetUserByID(id int) (*User, error) {
    u, err := db.FindUser(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, errors.UserNotFound // 直接返回预定义变量
        }
        return nil, &errors.Error{
            Code:    errors.ErrDatabase,
            Message: fmt.Sprintf("failed to get user %d from db: %v", id, err),
            Status:  500,
        }
    }
    return u, nil
}
  • 优先匹配已知底层错误(如 sql.ErrNoRows),映射为明确的业务错误变量
  • 对未知错误,用 &Error{...} 包装,保留原始错误作为 cause(可被 errors.Unwrap 获取)
  • 避免在包装时重复写 “failed to …” —— 预定义变量的 Message 已足够清晰,额外描述应放在日志里,而非错误值中

HTTP handler 中如何统一响应错误

在 handler 层,不要每个地方都写 if err != nil { w.WriteHeader(404); json.NewEncoder(w).Encode(...)} 。用中间件或封装的响应函数统一处理。

立即学习“go语言免费学习笔记(深入)”;

func JSONError(w http.ResponseWriter, err error, statusCode int) {
    var appErr *errors.Error
    if errors.As(err, &appErr) {
        statusCode = appErr.Status
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)

    json.NewEncoder(w).Encode(map[string]interface{}{
        "error": map[string]interface{}{
            "code":    appErr.Code,
            "message": appErr.Message,
        },
    })
}

// 使用示例
func userHandler(w http.ResponseWriter, r *http.Request) {
    u, err := GetUserByID(123)
    if err != nil {
        JSONError(w, err, 0) // 0 表示由 JSONError 内部决定状态码
        return
    }
    json.NewEncoder(w).Encode(u)
}
  • JSONError 通过 errors.As 提取自定义错误字段,其他错误(如 panic 捕获或 net/http 超时)走默认状态码
  • 不强制要求所有错误都必须是 *errors.Error,兼容标准库错误,避免过度约束
  • 如果项目用 Gin/Echo,可进一步封装成 c.AbortWithStatusJSON 的 wrapper,但核心逻辑不变
错误定义真正难的不是结构设计,而是团队能否坚持把新错误加到统一包里、而不是随手 fmt.Errorf 一把梭。一旦放开这个口子,三个月后你就会在三个不同文件里看到几乎一样的 "order not found" 字符串。