Go语言错误处理链式包装与自定义错误码体系设计

Go语言错误处理的工程化痛点

Go语言的错误处理采用显式返回值模式,函数调用链中每层都需要处理error,代码中充斥大量if err != nil判断。这种模式虽然避免了异常机制的隐式控制流跳转,但在多层调用场景下,错误信息缺乏上下文——底层返回的connection refused错误,到了顶层调用者已经丢失了”哪个服务、哪个接口、什么时间”的关键信息。Go 1.13引入的错误包装(Error Wrapping)机制和自定义错误码体系,是解决这个问题的工程化方案。

错误包装:errors.Is与errors.As

Go 1.13的fmt.Errorf支持%w动词包装错误,形成错误链:

package order

import (
    "errors"
    "fmt"
)

var (
    ErrOrderNotFound = errors.New("order not found")
    ErrOrderExpired  = errors.New("order expired")
)

func GetOrder(id string) (*Order, error) {
    order, err := repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("get order %s: %w", id, err)
    }
    if order.Status == "expired" {
        return nil, fmt.Errorf("order %s: %w", id, ErrOrderExpired)
    }
    return order, nil
}

调用方用errors.Is判断错误链中是否包含特定哨兵错误,errors.As提取特定类型的自定义错误:

order, err := order.GetOrder("ORD-001")
if err != nil {
    if errors.Is(err, order.ErrOrderNotFound) {
        // 处理订单不存在
        return http.StatusNotFound
    }
    if errors.Is(err, order.ErrOrderExpired) {
        // 处理订单过期
        return http.StatusGone
    }
    // 其他未知错误
    return http.StatusInternalServerError
}

errors.Is会递归遍历错误链,即使错误被多层包装也能正确匹配。这是相比直接err == sentinel判断的核心优势。

自定义错误码体系设计

微服务架构中,错误码需要满足三个要求:全局唯一、可机器解析、包含足够上下文。以下是一个完整的设计方案:

package apperr

import (
    "fmt"
    "net/http"
)

// ErrorCode 错误码类型
type ErrorCode string

const (
    // 通用错误 1xxxx
    CodeUnknown       ErrorCode = "10000"
    CodeInvalidParam  ErrorCode = "10001"
    CodeUnauthorized  ErrorCode = "10002"
    CodeForbidden     ErrorCode = "10003"
    CodeRateLimited   ErrorCode = "10004"

    // 订单模块 2xxxx
    CodeOrderNotFound ErrorCode = "20001"
    CodeOrderExpired  ErrorCode = "20002"
    CodeOrderConflict ErrorCode = "20003"

    // 支付模块 3xxxx
    CodePayInsufficient ErrorCode = "30001"
    CodePayFailed       ErrorCode = "30002"
    CodePayTimeout      ErrorCode = "30003"
)

// AppError 自定义错误类型
type AppError struct {
    Code    ErrorCode `json:"code"`
    Message string    `json:"message"`
    Detail  string    `json:"detail,omitempty"`
    Cause   error     `json:"-"`
}

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

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

// HTTPStatus 将错误码映射为HTTP状态码
func (e *AppError) HTTPStatus() int {
    switch {
    case e.Code == CodeInvalidParam:
        return http.StatusBadRequest
    case e.Code == CodeUnauthorized:
        return http.StatusUnauthorized
    case e.Code == CodeForbidden:
        return http.StatusForbidden
    case e.Code == CodeOrderNotFound:
        return http.StatusNotFound
    case e.Code == CodeOrderConflict:
        return http.StatusConflict
    case e.Code == CodeRateLimited:
        return http.StatusTooManyRequests
    default:
        return http.StatusInternalServerError
    }
}

// New 创建自定义错误
func New(code ErrorCode, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

// Wrap 包装底层错误
func Wrap(code ErrorCode, message string, cause error) *AppError {
    return &AppError{Code: code, Message: message, Cause: cause}
}

在业务代码中使用自定义错误码

package order

import "myapp/pkg/apperr"

func (s *Service) CreateOrder(req *CreateOrderReq) (*Order, error) {
    // 参数校验
    if req.UserID == "" {
        return nil, apperr.New(apperr.CodeInvalidParam, "user_id is required")
    }

    // 业务逻辑 - 包装底层错误
    user, err := s.userRepo.GetByID(req.UserID)
    if err != nil {
        return nil, apperr.Wrap(apperr.CodeOrderNotFound, "query user failed", err)
    }

    if user.Balance < req.Amount {
        return nil, apperr.New(apperr.CodePayInsufficient, 
            fmt.Sprintf("balance %.2f < amount %.2f", user.Balance, req.Amount))
    }

    order, err := s.repo.Create(&Order{
        UserID: req.UserID,
        Amount: req.Amount,
        Status: "pending",
    })
    if err != nil {
        return nil, apperr.Wrap(apperr.CodeOrderConflict, "create order failed", err)
    }
    return order, nil
}

HTTP层统一错误响应

Gin中间件中统一处理AppError,自动映射HTTP状态码和响应格式:

package middleware

import (
    "net/http"
    "myapp/pkg/apperr"

    "github.com/gin-gonic/gin"
)

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        err := c.Errors.Last()
        if err == nil {
            return
        }

        var appErr *apperr.AppError
        if errors.As(err.Err, &appErr) {
            c.JSON(appErr.HTTPStatus(), gin.H{
                "code":    appErr.Code,
                "message": appErr.Message,
                "detail":  appErr.Detail,
            })
            return
        }

        // 非AppError统一返回500
        c.JSON(http.StatusInternalServerError, gin.H{
            "code":    apperr.CodeUnknown,
            "message": "internal server error",
        })
    }
}

错误码与监控告警集成

错误码的核心价值在于可观测性。在日志和指标中注入错误码,可以精确统计每个错误的发生频次和分布:

// Prometheus指标采集
var errorCounter = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "app_errors_total",
    },
    []string{"code", "module"},
)

func RecordError(err error) {
    var appErr *apperr.AppError
    if errors.As(err, &appErr) {
        errorCounter.WithLabelValues(string(appErr.Code), "order").Inc()
    } else {
        errorCounter.WithLabelValues("10000", "unknown").Inc()
    }
}

基于错误码的告警规则可以做到精确到具体故障类型——当CodePayTimeout在5分钟内增长超过阈值时触发告警,而不是笼统的”5xx错误率升高”。

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

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

相关推荐