Go语言错误处理链与自定义错误类型设计模式

Go错误处理的设计哲学与基本模式

Go语言的错误处理采用显式返回值方式,函数通过error接口向调用方传递错误信息。与异常机制不同,Go要求开发者在每一步调用点处理或转发错误,虽然代码看起来冗长,但错误路径完全可见、可追踪、可控。

Go 1.13引入的错误链(error chain)机制和errors.Is/As函数,以及fmt.Errorf的%w动词,让错误处理从简单的字符串比较进化为结构化的错误链路追踪。这为自定义错误类型和错误包装提供了标准化的设计范式。

// 基本错误创建与比较
var ErrNotFound = errors.New("resource not found")

func GetUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ...", id)
    var u User
    if err := row.Scan(&u.Name, &u.Email); err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("get user %d: %w", id, ErrNotFound)
        }
        return nil, fmt.Errorf("get user %d: %w", id, err)
    }
    return &u, nil
}

自定义错误类型与结构化错误信息

当错误需要携带上下文信息(错误码、模块名、原始错误等)时,定义自定义错误类型是标准做法。自定义错误类型实现error接口,同时提供额外的类型化访问方法。

// 自定义错误类型
type AppError struct {
    Code    string // 业务错误码,如 "USER_001"
    Message string // 用户友好的错误描述
    Detail  string // 内部调试信息
    Err     error  // 原始错误
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 工厂函数
func NewAppError(code, message string, err error) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Err:     err,
    }
}

自定义错误类型的设计原则:Code字段使用常量而非magic string;Message面向最终用户,不含技术细节;Detail字段仅在日志中输出,不返回给客户端;Unwrap方法支持errors.Is/As的链式查找。

错误包装与errors.Is/As链式查找

fmt.Errorf配合%w动词创建包装错误,形成错误链。errors.Is沿链路递归查找目标错误,errors.As查找链路中特定类型的错误值:

func ProcessOrder(orderID string) error {
    inventory, err := checkInventory(orderID)
    if err != nil {
        return fmt.Errorf("process order %s: %w", orderID, err)
    }

    if err := reserveStock(inventory); err != nil {
        return fmt.Errorf("process order %s: %w", orderID, err)
    }

    if err := createPayment(orderID); err != nil {
        // 多层包装
        return fmt.Errorf("process order %s: %w", orderID,
            NewAppError("PAY_001", "payment failed", err))
    }
    return nil
}

// 调用方使用errors.Is/As做精确匹配
func HandleError(err error) {
    if errors.Is(err, ErrNotFound) {
        respondNotFound()
        return
    }

    var appErr *AppError
    if errors.As(err, &appErr) {
        respondWithCode(appErr.Code, appErr.Message)
        log.Printf("detail: %s", appErr.Detail)
        return
    }

    respondInternalError()
}

错误分组与sentinel错误设计

大型项目中错误码数量膨胀后,需要按模块组织sentinel错误。推荐按包/模块维度定义错误变量,用errors.Is实现分组判断:

// errors/user.go
var (
    ErrUserNotFound   = errors.New("user not found")
    ErrUserExists     = errors.New("user already exists")
    ErrUserDisabled   = errors.New("user disabled")
)

// errors/order.go
var (
    ErrOrderInvalid   = errors.New("order invalid")
    ErrOrderExpired   = errors.New("order expired")
    ErrOrderPaid      = errors.New("order already paid")
)

// errors/group.go - 错误分组
func IsUserError(err error) bool {
    return errors.Is(err, ErrUserNotFound) ||
           errors.Is(err, ErrUserExists) ||
           errors.Is(err, ErrUserDisabled)
}

func IsOrderError(err error) bool {
    return errors.Is(err, ErrOrderInvalid) ||
           errors.Is(err, ErrOrderExpired) ||
           errors.Is(err, ErrOrderPaid)
}

HTTP层可根据错误分组统一设置响应状态码:用户类错误返回400,订单类错误根据具体类型返回400或409。

错误处理中间件与日志链路追踪

在HTTP服务中,统一错误处理中间件是标准做法。中间件捕获handler返回的错误,根据类型生成HTTP响应,同时记录结构化日志:

func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var handler func(http.ResponseWriter, *http.Request) error

        err := handler(w, r)
        if err == nil {
            return
        }

        var appErr *AppError
        if errors.As(err, &appErr) {
            status := mapCodeToStatus(appErr.Code)
            respondJSON(w, status, map[string]string{
                "code":    appErr.Code,
                "message": appErr.Message,
            })
            slog.Error("request failed",
                "method", r.Method,
                "path", r.URL.Path,
                "code", appErr.Code,
                "error", appErr.Err,
            )
            return
        }

        respondJSON(w, 500, map[string]string{
            "code":    "INTERNAL",
            "message": "internal server error",
        })
        slog.Error("unhandled error",
            "method", r.Method,
            "path", r.URL.Path,
            "error", err,
        )
    })
}

slog结构化日志配合错误链追踪,可在日志系统中通过error code快速筛选和聚合同类错误,比传统的字符串匹配更精确。

Go 1.13之后的错误处理体系已经相当完善。关键原则:错误在创建时包装上下文,在处理时用Is/As做类型化判断,在日志中记录完整链路。避免在中间层吞掉错误(只记日志不返回),避免用字符串匹配判断错误类型,避免在error message中拼接敏感信息。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-cuo-wu-chu-li-lian-yu-zi-ding-yi-cuo-wu-lei-xing/

(0)
小编小编
上一篇 7小时前
下一篇 7小时前

相关推荐