Wrap 什么时候用NewXxx?WithCause 和Wrap 有什么区别?errors 包协作?exception 包提供了统一的业务异常处理机制,支持错误链追踪、错误分类、堆栈追踪等高级特性。
✨ HTTP友好:自动映射到HTTP状态码
🔗 错误链支持:兼容 Go 1.13+ 的 errors.Is/As
📊 错误分类:自动判断错误类型和是否可重试
🔍 堆栈追踪:可选的函数调用栈记录
🔒 线程安全:支持并发访问元数据
🎯 构建器模式:灵活的错误构造方式
✅ 完全向后兼容:保留所有旧版API
我们在进行接口调用时,需要根据接口的报错信息来决定怎么处理后续逻辑,比如:
通过为每一种异常定义独特的编码和标准结构,可以精准识别和处理各种业务异常
传统使用 fmt.Errorf 或 errors.New() 生成异常:
var ERR_TOKEN_EXPIRED = errors.New("用户Token已经过期")前端只能通过字符串匹配来辨别异常,容易误伤且不利于国际化:
{
"error": "用户Token已经过期"
}使用异常码后,通过独特的编码精准识别异常:
{
"code": 10001,
"reason": "访问过期, 请刷新",
"message": "用户Token已经过期"
}package main
import "github.com/infraboard/mcube/v2/exception"
func main() {
// 创建常见的业务异常
err := exception.NewNotFound("用户 %s 不存在", "alice")
// 添加元数据
err.WithMeta("user_id", "123")
// 获取HTTP状态码
statusCode := err.GetHttpCode() // 404
// 输出JSON格式
fmt.Println(err.ToJson())
}输出示例:
{
"service": "service_a",
"code": 404,
"reason": "资源未找到",
"message": "用户 alice 不存在",
"meta": {
"user_id": "123"
},
"data": null
}mcube 已经将一些常用的异常预先定义好了:
reasonMap = map[int]string{
CODE_UNAUTHORIZED: "认证失败",
CODE_NOT_FOUND: "资源未找到",
CODE_CONFLICT: "资源已经存在",
CODE_BAD_REQUEST: "请求不合法",
CODE_INTERNAL_SERVER_ERROR: "系统内部错误",
CODE_FORBIDDEN: "访问未授权",
CODE_UNKNOWN: "未知异常",
CODE_ACCESS_TOKEN_ILLEGAL: "访问令牌不合法",
CODE_REFRESH_TOKEN_ILLEGAL: "刷新令牌不合法",
CODE_OTHER_PLACE_LGOIN: "异地登录",
CODE_OTHER_IP_LOGIN: "异常IP登录",
CODE_OTHER_CLIENT_LOGIN: "用户已经通过其他端登录",
CODE_SESSION_TERMINATED: "会话结束",
CODE_ACESS_TOKEN_EXPIRED: "访问过期, 请刷新",
CODE_REFRESH_TOKEN_EXPIRED: "刷新过期, 请登录",
CODE_VERIFY_CODE_REQUIRED: "异常操作, 需要验证码进行二次确认",
CODE_PASSWORD_EXPIRED: "密码过期, 请找回密码或者联系管理员重置",
}创建异常:使用 exception.NewXXX() 快捷函数
判断异常:使用 exception.IsApiException() 进行类型判断
import (
"errors"
"testing"
"github.com/infraboard/mcube/v2/exception"
)
func TestNotFoundError(t *testing.T) {
// 创建异常
e := exception.NewNotFound("user %s not found", "alice")
t.Log(e.ToJson())
// 判断异常类型
t.Log(exception.IsApiException(e, exception.CODE_NOT_FOUND))
}异常返回示例:
{
"service": "service_a",
"code": 404,
"reason": "资源未找到",
"message": "user alice not found",
"meta": null,
"data": null
}当内置的全局异常不够用时,可以通过 NewApiException 创建业务专属异常:
import "github.com/infraboard/mcube/v2/exception"
const (
CODE_INSUFFICIENT_BALANCE = 100001
)
// 创建自定义异常
func NewErrInsufficientBalance(amount float64) *exception.ApiException {
return exception.NewApiException(CODE_INSUFFICIENT_BALANCE, "余额不足").
WithMessage("当前余额不足,需要 %.2f 元", amount)
}
// 判断是否为余额不足异常
func IsErrInsufficientBalance(err error) bool {
return exception.IsApiException(err, CODE_INSUFFICIENT_BALANCE)
}mcube 提供了一系列快捷创建函数,自动映射到对应的 HTTP 状态码:
| 函数 | HTTP状态码 | 说明 |
|---|---|---|
NewBadRequest(msg, args...) | 400 | 请求参数错误 |
NewUnauthorized(msg, args...) | 401 | 未认证 |
NewForbidden(msg, args...) | 403 | 无权限 |
NewNotFound(msg, args...) | 404 | 资源不存在 |
NewConflict(msg, args...) | 409 | 资源冲突 |
NewInternalServerError(msg, args...) | 500 | 服务器内部错误 |
使用示例:
// 参数验证失败
if username == "" {
return exception.NewBadRequest("用户名不能为空")
}
// 资源不存在
user, err := repo.FindByID(id)
if err != nil {
return exception.NewNotFound("用户 %s 不存在", id)
}
// 权限不足
if !user.HasPermission("admin") {
return exception.NewForbidden("需要管理员权限")
}错误链允许你包装底层错误并添加上下文信息,同时保留原始错误:
import (
"errors"
"database/sql"
"github.com/infraboard/mcube/v2/exception"
)
// 包装底层错误
dbErr := sql.ErrNoRows
apiErr := exception.Wrap(dbErr, exception.CODE_NOT_FOUND, "查询失败")
// 支持 errors.Is 判断
if errors.Is(apiErr, sql.ErrNoRows) {
// 可以追溯到原始错误
}
// 查看完整错误链
fmt.Println(apiErr.ErrorChainString())
// 输出: 查询失败: sql: no rows in result set → sql: no rows in result set// 包装错误
func Wrap(err error, code int, reason string) *ApiException
// 包装错误并格式化消息
func Wrapf(err error, code int, reason, format string, args ...any) *ApiException
// 获取原始错误
err.Unwrap() error
err.Cause() error
// 获取完整错误链
err.ErrorChain() []string
err.ErrorChainString() stringfunc (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 将数据库错误包装为业务异常
return nil, exception.NewNotFound("用户不存在").
WithMeta("user_id", id)
}
// 包装未知错误,保留错误链
return nil, exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "查询用户失败").
WithMeta("user_id", id).
WithStack()
}
return user, nil
}构建器模式提供了更灵活的异常构造方式,适合构建复杂的异常对象:
// 使用构建器创建复杂异常
err := exception.NewBuilder(exception.CODE_BAD_REQUEST).
WithReason("参数验证失败").
WithMessage("邮箱格式不正确").
WithMeta("field", "email").
WithTraceID("trace-123").
WithStack(). // 添加堆栈追踪
Build()// 创建构建器
builder := exception.NewBuilder(code)
// 链式调用设置各种属性
builder.
WithReason(reason). // 设置异常原因
WithMessage(message). // 设置消息
WithMessagef(format, args...). // 格式化消息
WithHTTPCode(httpCode). // 设置HTTP状态码
WithMeta(key, value). // 添加元数据
WithData(data). // 设置响应数据
WithCause(err). // 设置原因错误
WithService(serviceName). // 设置服务名
WithTraceID(traceID). // 设置追踪ID
WithRequestID(requestID). // 设置请求ID
WithStack(). // 添加堆栈追踪
Build() // 构建异常对象异常包支持自动错误分类,帮助判断错误类型和是否可重试:
const (
ErrorTypeUnknown // 未知
ErrorTypeClient // 客户端错误(4xx)
ErrorTypeServer // 服务端错误(5xx)
ErrorTypeAuth // 认证授权错误
ErrorTypeValidation // 验证错误
ErrorTypeNotFound // 资源不存在
ErrorTypeConflict // 资源冲突
)err := exception.NewInternalServerError("系统异常")
// 判断错误类型
if err.IsServerError() {
// 服务端错误,记录详细日志
log.Error().
Str("stack", err.GetStack()).
Msg("服务器内部错误")
}
if err.IsRetryable() {
// 可以重试的错误
return retryOperation()
}
// 获取错误类型
errType := err.Type() // ErrorTypeServer// 类型判断
err.Type() ErrorType // 获取错误类型
err.IsRetryable() bool // 是否可重试
err.IsClientError() bool // 是否客户端错误(4xx)
err.IsServerError() bool // 是否服务端错误(5xx)
err.IsAuthError() bool // 是否认证错误func (c *Client) CallWithRetry(ctx context.Context, fn func() error) error {
var lastErr error
for i := 0; i < 3; i++ {
err := fn()
if err == nil {
return nil
}
lastErr = err
// 检查是否可重试
if apiErr, ok := err.(*exception.ApiException); ok {
if !apiErr.IsRetryable() {
// 客户端错误或永久性错误,不重试
return err
}
}
// 等待后重试
time.Sleep(time.Second * time.Duration(i+1))
}
return exception.Wrap(lastErr, exception.CODE_INTERNAL_SERVER_ERROR, "重试3次后仍失败")
}堆栈追踪可以帮助快速定位错误来源,适合在关键错误路径使用:
// 添加堆栈信息(默认3层调用栈)
err := exception.NewInternalServerError("系统错误").WithStack()
// 指定调用栈深度
err := exception.NewInternalServerError("系统错误").WithStackDepth(10)
// 获取堆栈信息
stack := err.GetStack()
fmt.Println(stack)// ✅ 关键错误或难以复现的问题
if err := criticalOperation(); err != nil {
return exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "关键操作失败").
WithStack()
}
// ✅ 系统级错误
if err := db.Connect(); err != nil {
return exception.NewInternalServerError("数据库连接失败").
WithCause(err).
WithStack()
}
// ❌ 避免:常见业务错误收集堆栈(性能开销)
if user == nil {
return exception.NewNotFound("用户不存在") // 无需 WithStack()
}TraceID 和 RequestID 是直接字段,类型安全,自动序列化到 JSON:
// 设置追踪ID(用于分布式追踪)
err.WithTraceID("trace-xxx-xxx")
// 设置请求ID
err.WithRequestID("req-xxx-xxx")
// 获取方法
traceID := err.GetTraceID()
requestID := err.GetRequestID()
// 也可以直接访问字段
traceID := err.TraceID
requestID := err.RequestID堆栈跟踪有一定性能开销,建议按需使用:
// ✅ 仅在关键路径或难以调查的错误时收集堆栈
if criticalOperation {
return exception.NewInternalServerError("critical error").WithStack()
}
// ✅ 简单的业务错误无需堆栈
return exception.NewBadRequest("invalid input")Meta 并发访问已加锁保护,但频繁操作时考虑批量设置:
// ✅ 好的做法:构建时一次性设置
return exception.NewBadRequest("error").
WithMeta("field1", val1).
WithMeta("field2", val2).
WithMeta("field3", val3)
// ❌ 避免:多处并发修改同一个异常实例错误链遍历是 O(n) 操作,避免过深的嵌套:
// ✅ 合理:2-3层嵌套
err := exception.Wrap(lowLevelErr, exception.CODE_INTERNAL_SERVER_ERROR, "high level context")
// ⚠️ 避免:过深嵌套(如循环中多次Wrap)func (h *Handler) GetUser(c *gin.Context) {
userID := c.Param("id")
user, err := h.service.GetUser(c.Request.Context(), userID)
if err != nil {
// 自动映射到HTTP状态码
if apiErr, ok := err.(*exception.ApiException); ok {
c.JSON(apiErr.GetHttpCode(), gin.H{"error": apiErr.Error()})
} else {
c.JSON(500, gin.H{"error": "Internal Server Error"})
}
return
}
c.JSON(200, user)
}
// Service层
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 404错误
return nil, exception.NewNotFound("用户不存在").
WithMeta("user_id", id)
}
// 500错误,保留原始错误链
return nil, exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "查询用户失败").
WithMeta("user_id", id).
WithStack()
}
return user, nil
}func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*Order, error) {
// 调用库存服务
if err := s.inventoryClient.Reserve(ctx, req.Items); err != nil {
if apiErr, ok := err.(*exception.ApiException); ok {
// 保留原始错误码并添加上下文
return nil, exception.Wrap(apiErr, apiErr.Code(), "库存预留失败").
WithMeta("order_items", req.Items)
}
return nil, exception.NewInternalServerError("库存服务异常").
WithCause(err).
WithStack()
}
// 调用支付服务
if err := s.paymentClient.Charge(ctx, req.Amount); err != nil {
// 回滚库存
s.inventoryClient.Rollback(ctx, req.Items)
return nil, exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "支付失败").
WithMeta("amount", req.Amount)
}
// 创建订单...
return order, nil
}func handleError(err error) {
if apiErr, ok := err.(*exception.ApiException); ok {
log.Error().
Str("error", apiErr.Error()).
Str("reason", apiErr.Reason()).
Int("code", apiErr.Code()).
Str("trace_id", apiErr.GetTraceID()).
Interface("error_chain", apiErr.ErrorChain()).
Str("stack", apiErr.GetStack()).
Msg("操作失败")
}
}// ✅ 使用语义明确的构造函数
return exception.NewBadRequest("用户名不能为空")
// ✅ 使用Builder构建复杂错误
return exception.NewBuilder(exception.CODE_BAD_REQUEST).
WithReason("参数验证失败").
WithMeta("field", "email").
WithMeta("reason", "格式错误").
Build()
// ❌ 避免:通用错误码损失语义
return exception.NewApiException(400, "错误")// ✅ 跨层调用时添加上下文
func (s *Service) Process(id string) error {
data, err := s.repo.Get(id)
if err != nil {
// Wrap保留底层错误,添加业务上下文
return exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "处理失败")
}
return nil
}
// ❌ 避免:包装已经是APIException的错误而不添加信息
if apiErr := s.doSomething(); apiErr != nil {
return exception.Wrap(apiErr, apiErr.Code(), "") // 无意义
}// ✅ 添加有助于调查的信息
return exception.NewNotFound("订单不存在").
WithMeta("order_id", orderID).
WithMeta("user_id", userID).
WithMeta("query_time", time.Now())
// ❌ 避免:敏感信息泄露
return exception.NewUnauthorized("认证失败").
WithMeta("password", userPassword) // 危险!// ✅ 关键错误或难以复现的问题
if err := criticalOperation(); err != nil {
return exception.Wrap(err, exception.CODE_INTERNAL_SERVER_ERROR, "关键操作失败").
WithStack()
}
// ✅ 系统级错误
if err := db.Connect(); err != nil {
return exception.NewInternalServerError("数据库连接失败").
WithCause(err).
WithStack()
}
// ❌ 避免:常见业务错误收集堆栈(性能开销)
if user == nil {
return exception.NewNotFound("用户不存在").WithStack() // 不必要
}// ✅ 为可重试错误添加标记
if networkErr := callRemote(); networkErr != nil {
return exception.NewServiceUnavailable("服务暂时不可用").
WithCause(networkErr)
}
// ✅ 明确客户端错误
if !validateInput(req) {
return exception.NewBadRequest("输入验证失败")
}Wrap 什么时候用NewXxx?A:
Wrap: 当你有一个底层错误需要添加上下文时NewXxx: 当你创建新的业务错误时// Wrap - 包装已有错误
dbErr := db.Query(...)
return exception.Wrap(dbErr, exception.CODE_INTERNAL_SERVER_ERROR, "查询用户失败")
// NewXxx - 创建新错误
if user == nil {
return exception.NewNotFound("用户不存在")
}WithCause 和Wrap 有什么区别?A:
Wrap: 包装错误时会尝试保留原错误的HTTP状态码(如果是APIException)WithCause: 仅存储原始错误,不影响当前错误的状态码// Wrap - 保留底层APIException的状态码
lowLevelErr := exception.NewNotFound("resource not found") // 404
err := exception.Wrap(lowLevelErr, lowLevelErr.Code(), "operation failed")
// err.Code() == 404
// WithCause - 使用新的状态码
err2 := exception.NewInternalServerError("system error"). // 500
WithCause(lowLevelErr)
// err2.Code() == 500A: 错误链本身开销很小,但堆栈跟踪有一定开销:
WithStack()A: 使用 GetStack() 和 ErrorChain() 获取详细信息:
if apiErr, ok := err.(*exception.ApiException); ok {
log.Error().
Str("error", apiErr.Error()).
Str("trace_id", apiErr.GetTraceID()).
Interface("error_chain", apiErr.ErrorChain()).
Str("stack", apiErr.GetStack()).
Msg("操作失败")
}A:
WithMeta/GetMeta: 线程安全(已加锁)errors 包协作?A: 完全兼容 Go 1.13+ 错误处理:
// errors.Is
if errors.Is(err, sql.ErrNoRows) { ... }
// errors.As
var apiErr *exception.ApiException
if errors.As(err, &apiErr) {
log.Printf("HTTP Code: %d", apiErr.Code())
}
// 错误链遍历
for e := err; e != nil; e = errors.Unwrap(e) {
log.Println(e)
}所有旧版 API 完全保留,现有代码无需修改:
// ✅ 所有旧的创建方法继续支持
err := exception.NewNotFound("resource not found")
err.WithMeta("key", "value")
err.WithData(data)
// ✅ 所有旧的判断方法继续支持
if exception.IsNotFoundError(err) {
// ...
}
if exception.IsApiException(err, exception.CODE_NOT_FOUND) {
// ...
}
// ✅ 所有常量继续可用
code := exception.CODE_NOT_FOUND