errors.md - maoxiaoyue/hypgo GitHub Wiki

pkg/errors — Typed Error Catalog

預定義結構化錯誤碼系統,讓 AI 和人都能一致理解和使用錯誤。

設計理念

傳統做法每個 handler 各自定義錯誤訊息,AI 無法預測回傳格式。Typed Error Catalog 將錯誤定義為全域常數,統一格式且可被 Manifest 收錄:

❌ c.JSON(404, map[string]string{"error": "user not found"})     ← 隨意格式
✅ errors.AbortWithAppError(c, errors.ErrUserNotFound.With("id", 42))  ← 結構化

快速上手

定義錯誤碼

package apperrors

import "github.com/maoxiaoyue/hypgo/pkg/errors"

var (
    ErrUserNotFound = errors.Define("E1001", 404, "User not found", "users")
    ErrUserExists   = errors.Define("E1002", 409, "User already exists", "users")
    ErrInvalidEmail = errors.Define("E1003", 422, "Invalid email format", "users")
)

在 handler 中使用

func getUser(c *hypcontext.Context) {
    id := c.Param("id")
    user, err := userService.FindByID(id)
    if err != nil {
        errors.AbortWithAppError(c, apperrors.ErrUserNotFound.With("id", id))
        return
    }
    c.JSON(200, user)
}

回應格式

{
    "code": "E1001",
    "message": "User not found",
    "details": {
        "id": "42"
    }
}

核心 API

Define — 定義錯誤

errors.Define(code, httpStatus, message, category) *AppError

自動註冊到全域 Catalog,通常在 package-level var 區塊中使用。

AppError 方法

// 附加上下文(回傳副本,原始不變)
err.With("field", "email")
err.WithDetail("reason", "already exists")
err.WithDetails(map[string]any{"a": 1, "b": 2})
err.WithMessage("Custom message")

// 轉為 JSON(給 HTTP response 用)
err.JSON()  // → map[string]any{"code":"E1001","message":"...","details":{...}}

// 錯誤比對(只比 Code,忽略 Details)
errors.Is(err1, err2)  // true if same code

Context 整合

// 中斷請求並回傳結構化錯誤
errors.AbortWithAppError(c, err)

// 回傳錯誤但不中斷 middleware 鏈
errors.RespondError(c, err)

Catalog 查詢

catalog := errors.GlobalCatalog()

// 查詢所有已定義的錯誤
all := catalog.All()

// 根據 code 查詢
err, ok := catalog.Get("E1001")

// 根據分類查詢
authErrors := catalog.ByCategory("auth")

不可變設計

AppErrorWith*() 方法永遠回傳副本,原始定義不會被修改:

original := errors.Define("E1", 404, "Not found", "general")
copy1 := original.With("id", 1)
copy2 := original.With("id", 2)

// original.Details == nil(未被修改)
// copy1.Details == {"id": 1}
// copy2.Details == {"id": 2}

預定義錯誤

Code HTTP Message Category
E0001 404 Resource not found general
E0002 400 Bad request general
E0003 500 Internal server error general
E0004 405 Method not allowed general
E1001 422 Validation failed validation
E1002 400 Missing required field validation
E1003 400 Invalid format validation
E2001 401 Authentication required auth
E2002 403 Permission denied auth
E2003 401 Token expired auth

架構

pkg/errors/
├── catalog.go       AppError、Define()、Catalog、預定義錯誤
├── context.go       AbortWithAppError()、RespondError()
└── catalog_test.go  19 個單元測試

測試

go test ./pkg/errors/... -v