response

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 3 Imported by: 0

README

common/response

统一 HTTP JSON 响应封装、应用错误处理、状态码规范和 i18n 消息 key 常量。

设计理念

  • 统一信封:所有 API 响应遵循 {"code", "msg", "data"} 格式
  • 稳定错误码:字符串 Code(如 "NOT_FOUND")供客户端分支判断,不随版本变更语义
  • 数字业务码200(成功)、1000-1999(客户端错误)、2000-2999(系统错误)
  • i18n 优先:错误存储消息 key,在渲染时根据请求 locale 解析为本地化文本
  • 解耦:核心模块不依赖 gin;gin 集成在 common/response/gin 子模块

响应格式

成功
{"code": 200, "msg": "success", "data": {...}}
错误
{"code": 1001, "msg": "未找到", "error": "NOT_FOUND", "data": null, "details": null}

快速使用

服务层 — 返回 AppError
import "github.com/LingByte/ling-base/common/response"

func GetUser(id string) (*User, error) {
    user, err := db.Find(id)
    if err != nil {
        return nil, response.WrapErr(response.CodeInternal, err)
    }
    if user == nil {
        return nil, response.NewI18n(response.CodeNotFound, response.KeyNotFound)
    }
    return user, nil
}
HTTP 层 — gin 集成
import ginresp "github.com/LingByte/ling-base/common/response/gin"

func Handler(c *gin.Context) {
    user, err := service.GetUser(id)
    if err != nil {
        ginresp.WriteError(c, err)  // 自动转换为 AppError 信封
        return
    }
    ginresp.Success(c, user)
}
i18n 集成
import (
    "github.com/LingByte/ling-base/i18n"
    ginresp "github.com/LingByte/ling-base/common/response/gin"
    i18ngin "github.com/LingByte/ling-base/i18n/gin"
)

func setupRouter(manager *i18n.Manager) *gin.Engine {
    r := gin.Default()
    
    // i18n 中间件 — 从 Accept-Language 解析 locale
    r.Use(i18ngin.Middleware(manager))
    
    // 设置 response 的消息解析器
    ginresp.Resolver = ginresp.ResolverFunc(func(key string, args ...any) string {
        locale := i18ngin.GetLocale(c)  // 从 context 获取
        return manager.T(locale, key, args...)
    })
    
    return r
}

错误码规范

字符串 Code(稳定标识符)
Code HTTP 数字码 说明
BAD_REQUEST 400 1000 请求参数错误
VALIDATION_FAILED 400 1011 数据校验失败
UNAUTHORIZED 401 1002 未授权
AUTH_FAILED 401 1002 认证失败
CREDENTIAL_INVALID 401 1002 凭证无效
FORBIDDEN 403 1003 禁止访问
TENANT_MISMATCH 403 1006 租户不匹配
NOT_FOUND 404 1001 资源未找到
CONFLICT 409 1004 资源冲突
DUPLICATE 409 1010 资源重复
RATE_LIMITED 429 1005 请求频率限制
QUOTA_EXCEEDED 402 1007 配额超限
UPSTREAM_TIMEOUT 504 1008 上游超时
SERVICE_UNAVAILABLE 503 1009 服务不可用
PROVIDER_ERROR 503 2002 供应商错误
INTERNAL 500 2000 内部错误
数字业务码分段
范围 用途
200 成功
1000-1099 通用业务错误
1100-1199 认证错误
1200-1299 租户错误
1300-1399 权限错误
2000-2999 系统错误
3000+ 应用自定义(预留)

i18n 消息 Key 常量

// common.*
response.KeySuccess           // "common.success"
response.KeyNotFound           // "common.not_found"
response.KeyUnauthorized       // "common.unauthorized"
response.KeyForbidden          // "common.forbidden"
response.KeyInvalidParams      // "common.invalid_params"
response.KeyConflict           // "common.conflict"
response.KeyRateLimited        // "common.rate_limited"
response.KeyInternalError      // "common.internal_error"

// auth.*
response.KeyAuthInvalidCredentials  // "auth.invalid_credentials"
response.KeyAuthMissingToken        // "auth.missing_token"
response.KeyAuthInvalidToken        // "auth.invalid_token"

// tenant.*
response.KeyTenantNotFound          // "tenant.not_found"
response.KeyTenantEmailExists       // "tenant.email_exists"

// perm.*
response.KeyPermInsufficient        // "perm.insufficient"

// validation.*
response.KeyValidationRequired      // "validation.required"
response.KeyValidationEmail         // "validation.email"
response.KeyValidationMin           // "validation.min"

AppError 构造器

// 从 Code 创建(i18n 在渲染时解析)
err := response.Err(response.CodeNotFound)

// 带直接消息
err := response.New(response.CodeBadRequest, "invalid email format")

// 格式化消息
err := response.Newf(response.CodeValidation, "field %s is required", "email")

// 带 i18n key 和参数
err := response.NewI18n(response.CodeNotFound, response.KeyNotFound)

// 包装底层错误
err := response.Wrap(response.CodeInternal, "db query failed", dbErr)
err := response.WrapErr(response.CodeInternal, dbErr)
err := response.WrapI18n(response.CodeNotFound, response.KeyNotFound, dbErr)

Builder 方法

err := response.Err(response.CodeBadRequest).
    WithStatus(422).                    // 覆盖 HTTP 状态码
    WithDetails(map[string]any{         // 附加结构化详情
        "field": "email",
        "reason": "empty",
    }).
    WithCause(originalErr)              // 附加底层错误

MessageResolver

MessageResolver 接口将 i18n key 解析为本地化字符串:

type MessageResolver interface {
    Resolve(key string, args ...any) string
}

内置实现:

  • StaticResolver — map 内存查找,适合测试和简单应用
  • ChainResolver — 多级回退链
  • NoopResolver — 返回 key 本身(默认)
  • ResolverFunc — 函数适配器
resolver := &response.StaticResolver{
    Messages: map[string]string{
        "common.not_found": "未找到",
        "common.forbidden": "禁止访问",
    },
}
envelope := response.ErrorEnvelope(appErr, resolver)

分页

// 构造分页响应
page := response.NewPage(users, total, pageNum, pageSize)

// 返回
ginresp.Success(c, page)
// → {"code": 200, "msg": "success", "data": {"list": [...], "total": 100, "page": 1, "size": 20, "total_page": 5}}

Gin 集成

函数 说明
gin.Success(c, data) 200 成功响应
gin.SuccessI18n(c, key, data, args...) 200 带本地化消息
gin.Created(c, data) 201 创建成功
gin.NoContent(c) 204 无内容
gin.WriteError(c, err) 自动转换并渲染错误
gin.Fail(c, msg, data) 500 内部错误
gin.FailWithCode(c, code, msg, data) 指定 Code 的错误
gin.FailI18n(c, key, data, args...) 本地化错误
gin.FailAppError(c, ae) 直接渲染 AppError
gin.AbortWithStatusJSON(c, status, err) 中止并返回错误
gin.Recovery() panic 恢复中间件

errors.Is / errors.As 支持

err := response.Err(response.CodeNotFound)

// 比较 Code
if errors.Is(err, response.Err(response.CodeNotFound)) {
    // handle not found
}

// 提取 AppError
var ae *response.AppError
if errors.As(err, &ae) {
    fmt.Println(ae.Code, ae.HTTPStatus)
}

License

MIT

Documentation

Overview

Package response provides unified HTTP JSON envelopes, application errors, status code conventions, and i18n message key constants for ling-base APIs.

Success envelope

{"code": 200, "msg": "success", "data": ...}

Error envelope

{"code": 1001, "msg": "Not found", "error": "NOT_FOUND", "data": null}

Service layers return *AppError; HTTP handlers render it once via the gin helpers in the response/gin subpackage or via ErrorEnvelope.

i18n

AppError stores an i18n message key (MsgKey) and optional format arguments (MsgArgs). At render time, a MessageResolver translates the key to a localized string. The response/gin subpackage integrates with the i18n.Manager to resolve messages based on the request locale.

Error codes

Two parallel code systems are used:

  • Code (string): stable identifier like "NOT_FOUND" — clients branch on this.
  • Numeric code (int): business code like 1001 — returned in the "code" field of the envelope.

See codes.go for the full constant set and keys.go for i18n keys.

Index

Constants

View Source
const (
	CodeSuccess = 200

	// Business errors (1000-1999)
	CodeNumInvalidParams    = 1000
	CodeNumNotFound         = 1001
	CodeNumUnauthorized     = 1002
	CodeNumForbidden        = 1003
	CodeNumConflict         = 1004
	CodeNumRateLimited      = 1005
	CodeNumTenantMismatch   = 1006
	CodeNumQuotaExceeded    = 1007
	CodeNumUpstreamTimeout  = 1008
	CodeNumServiceUnavail   = 1009
	CodeNumDuplicate        = 1010
	CodeNumValidationFailed = 1011

	// Auth errors (1100-1199)
	CodeNumInvalidCredentials = 1100
	CodeNumMissingToken       = 1104
	CodeNumInvalidToken       = 1105

	// Tenant errors (1200-1299)
	CodeNumRegisterDisabled = 1200
	CodeNumEmailExists      = 1201
	CodeNumTenantNotFound   = 1203
	CodeNumTenantSuspended  = 1204
	CodeNumUserUnavailable  = 1205

	// Permission errors (1300-1399)
	CodeNumPermInsufficient = 1300

	// System errors (2000-2999)
	CodeNumInternal        = 2000
	CodeNumDatabaseUnavail = 2001
	CodeNumProviderErr     = 2002
)

Numeric business codes returned in the "code" field of envelopes. 200 = success; 1000-1999 = client/business errors; 2000-2999 = system errors. Ranges above 3000 are reserved for application-specific codes.

View Source
const (
	// common.* — generic success/error messages
	KeySuccess            = "common.success"
	KeyCreated            = "common.created"
	KeyUpdated            = "common.updated"
	KeyDeleted            = "common.deleted"
	KeyInvalidParams      = "common.invalid_params"
	KeyInvalidBody        = "common.invalid_body"
	KeyNotFound           = "common.not_found"
	KeyUnauthorized       = "common.unauthorized"
	KeyForbidden          = "common.forbidden"
	KeyConflict           = "common.conflict"
	KeyRateLimited        = "common.rate_limited"
	KeyInternalError      = "common.internal_error"
	KeyServiceUnavailable = "common.service_unavailable"
	KeyQuotaExceeded      = "common.quota_exceeded"
	KeyUpstreamTimeout    = "common.upstream_timeout"
	KeyTenantMismatch     = "common.tenant_mismatch"
	KeyDuplicate          = "common.duplicate"

	// auth.* — authentication
	KeyAuthInvalidCredentials = "auth.invalid_credentials"
	KeyAuthMissingToken       = "auth.missing_token"
	KeyAuthInvalidToken       = "auth.invalid_token"
	KeyAuthEmailNotRegistered = "auth.email_not_registered"
	KeyAuthEmailSameAsCurrent = "auth.email_same_as_current"

	// tenant.* — tenant/organization
	KeyTenantRegisterDisabled = "tenant.register_disabled"
	KeyTenantEmailExists      = "tenant.email_exists"
	KeyTenantNotFound         = "tenant.not_found"
	KeyTenantSuspended        = "tenant.suspended"
	KeyTenantUserUnavailable  = "tenant.user_unavailable"

	// perm.* — permissions
	KeyPermInsufficient = "perm.insufficient"

	// validation.* — field validation
	KeyValidationRequired        = "validation.required"
	KeyValidationEmail           = "validation.email"
	KeyValidationMin             = "validation.min"
	KeyValidationMax             = "validation.max"
	KeyValidationUsernameShort   = "validation.username_short"
	KeyValidationUsernameFormat  = "validation.username_format"
	KeyValidationPasswordShort   = "validation.password_short"
	KeyValidationCaptchaRequired = "validation.captcha_required"
	KeyValidationCaptchaInvalid  = "validation.captcha_invalid"
)

Message key constants for i18n. These follow the dotted convention "domain.message" and are intended to be resolved by an i18n.Manager or any MessageResolver implementation.

Using keys (not hardcoded text) in errors lets the rendering layer localize messages at the last moment based on the request locale.

Variables

This section is empty.

Functions

func EnvelopeData

func EnvelopeData(data any) map[string]any

EnvelopeData wraps any value in a map under the "data" key. This is a convenience for handlers that need to return extra metadata.

func EnvelopeError

func EnvelopeError(msg string) map[string]any

EnvelopeError wraps an error message in a simple map for ad-hoc use.

func ErrCodeFor

func ErrCodeFor(code Code) int

ErrCodeFor returns the numeric business code for a string Code.

func FormatDetails

func FormatDetails(ae *AppError) string

FormatDetails converts Details to a flat "key=value" string for logging. Returns empty string if Details is nil or empty.

func HTTPStatusFor

func HTTPStatusFor(code Code) int

HTTPStatusFor returns the default HTTP status code for a Code.

func HTTPStatusOf

func HTTPStatusOf(ae *AppError) int

HTTPStatusOf returns the HTTP status for an AppError, using the explicit override if set, otherwise the default for the Code.

func I18nKeyFor

func I18nKeyFor(code Code) string

I18nKeyFor returns the default i18n message key for a Code. The key can be passed to a MessageResolver to obtain a localized string.

func IsAppError

func IsAppError(err error) bool

IsAppError reports whether err is an *AppError.

Types

type AppError

type AppError struct {
	Code       Code           // Stable string identifier (e.g. "NOT_FOUND")
	MsgKey     string         // i18n message key (e.g. "common.not_found")
	MsgArgs    []any          // Arguments for i18n message formatting
	Message    string         // Direct message (non-i18n fallback)
	HTTPStatus int            // HTTP status code (0 = auto from Code)
	Cause      error          // Wrapped underlying error
	Details    map[string]any // Additional structured details
}

AppError is the unified application error shape. Service layers return *AppError; HTTP handlers render it once via the gin helpers or Envelope/HTTPStatusOf.

AppError supports two message paths:

  • i18n path: MsgKey + MsgArgs are resolved at render time via a MessageResolver (e.g. i18n.Manager). This is the recommended path for user-facing errors.
  • direct path: Message is used as-is when MsgKey is empty or no resolver is available.

func AsAppError

func AsAppError(err error) *AppError

AsAppError is a convenience that wraps From and returns the result even for nil errors (returning a generic internal error). Useful when a non-nil *AppError is required.

func Err

func Err(code Code) *AppError

Err creates an AppError from a Code only. The user-facing text is resolved at render time using the default i18n key for the Code.

func From

func From(err error) *AppError

From converts any error to *AppError. If err is already an *AppError it is returned as-is. nil input returns nil. Unknown errors become CodeInternal.

func New

func New(code Code, message string) *AppError

New constructs an AppError with an explicit (non-i18n) message.

func NewI18n

func NewI18n(code Code, msgKey string, args ...any) *AppError

NewI18n constructs an AppError whose user-facing text is resolved at render time via a MessageResolver.

func Newf

func Newf(code Code, format string, args ...any) *AppError

Newf is New with a formatted message.

func Wrap

func Wrap(code Code, message string, cause error) *AppError

Wrap attaches a cause to an AppError with an explicit message.

func WrapErr

func WrapErr(code Code, cause error) *AppError

WrapErr creates an AppError from a code and underlying error. The user-facing text is resolved at render time.

func WrapI18n

func WrapI18n(code Code, msgKey string, cause error, args ...any) *AppError

WrapI18n attaches a cause; user text comes from msgKey at render time.

func (*AppError) Error

func (e *AppError) Error() string

Error returns a human-readable string. If Message is set it is used; otherwise the MsgKey is returned so there is always a non-empty value.

func (*AppError) Is

func (e *AppError) Is(target error) bool

Is reports whether target is an *AppError with the same Code. This enables errors.Is(err, response.Err(response.CodeNotFound)).

func (*AppError) NumCode

func (ae *AppError) NumCode() int

NumCode returns the numeric business code for an AppError.

func (*AppError) Unwrap

func (e *AppError) Unwrap() error

Unwrap returns the wrapped cause for errors.Is / errors.As support.

func (*AppError) WithArg

func (e *AppError) WithArg(arg any) *AppError

WithArg appends a single i18n message argument.

func (*AppError) WithArgs

func (e *AppError) WithArgs(args ...any) *AppError

WithArgs sets the i18n message arguments.

func (*AppError) WithCause

func (e *AppError) WithCause(cause error) *AppError

WithCause attaches a wrapped underlying error.

func (*AppError) WithDetails

func (e *AppError) WithDetails(d map[string]any) *AppError

WithDetails attaches structured details to the error.

func (*AppError) WithStatus

func (e *AppError) WithStatus(status int) *AppError

WithStatus overrides the HTTP status code.

type ChainResolver

type ChainResolver struct {
	Resolvers []MessageResolver
}

ChainResolver tries each resolver in order, returning the first non-empty result. If all resolvers return empty, the key is returned.

func (*ChainResolver) Resolve

func (r *ChainResolver) Resolve(key string, args ...any) string

Resolve tries each resolver in order.

type Code

type Code string

Code is a stable, human-readable business error identifier. Clients branch on the string value; do not reuse a Code's semantics across releases.

const (
	CodeBadRequest        Code = "BAD_REQUEST"
	CodeUnauthorized      Code = "UNAUTHORIZED"
	CodeForbidden         Code = "FORBIDDEN"
	CodeNotFound          Code = "NOT_FOUND"
	CodeConflict          Code = "CONFLICT"
	CodeDuplicate         Code = "DUPLICATE"
	CodeRateLimited       Code = "RATE_LIMITED"
	CodeQuotaExceeded     Code = "QUOTA_EXCEEDED"
	CodeValidation        Code = "VALIDATION_FAILED"
	CodeTenantMismatch    Code = "TENANT_MISMATCH"
	CodeAuthFailed        Code = "AUTH_FAILED"
	CodeCredentialInvalid Code = "CREDENTIAL_INVALID"
	CodeUpstreamTimeout   Code = "UPSTREAM_TIMEOUT"
	CodeServiceUnavail    Code = "SERVICE_UNAVAILABLE"
	CodeProviderError     Code = "PROVIDER_ERROR"
	CodeInternal          Code = "INTERNAL"
)

Standard stable string codes. These are the canonical identifiers returned in the "error" field of error envelopes.

func CodeForHTTPStatus

func CodeForHTTPStatus(status int) Code

CodeForHTTPStatus returns the default string Code for an HTTP status.

type ErrorResponse

type ErrorResponse struct {
	Code    int            `json:"code"`  // numeric business code
	Message string         `json:"msg"`   // localized or direct message
	Error   string         `json:"error"` // stable string Code
	Data    any            `json:"data"`  // optional payload (usually nil)
	Details map[string]any `json:"details,omitempty"`
}

ErrorResponse is the standard JSON error envelope.

{"code": 1001, "msg": "Not found", "error": "NOT_FOUND", "data": null, "details": null}

func ErrorEnvelope

func ErrorEnvelope(ae *AppError, resolver MessageResolver) *ErrorResponse

ErrorEnvelope builds an ErrorResponse from an AppError using a MessageResolver to localize the message. If resolver is nil, the AppError.Message or MsgKey is used as the message.

type MessageResolver

type MessageResolver interface {
	Resolve(key string, args ...any) string
}

MessageResolver resolves an i18n message key to a localized string. Implementations are typically backed by an i18n.Manager, but any source (static map, database, remote service) can satisfy this interface.

The args are optional format arguments. If the resolved template contains verbs (e.g. %d, %s), the implementation should apply them.

var NoopResolver MessageResolver = ResolverFunc(func(key string, _ ...any) string {
	return key
})

NoopResolver always returns the key unchanged. It is the default when no resolver is configured.

type Page

type Page struct {
	List      any   `json:"list"`
	Total     int64 `json:"total"`
	Page      int   `json:"page"`
	Size      int   `json:"size"`
	TotalPage int   `json:"total_page"`
}

Page is a paginated list payload, intended to be embedded in Response.Data.

{"code": 200, "msg": "success", "data": {"list": [...], "total": 100, "page": 1, "size": 20, "total_page": 5}}

func NewPage

func NewPage(list any, total int64, page, size int) *Page

NewPage constructs a Page payload and computes TotalPage.

type ResolverFunc

type ResolverFunc func(key string, args ...any) string

ResolverFunc is a function adapter for MessageResolver.

func (ResolverFunc) Resolve

func (f ResolverFunc) Resolve(key string, args ...any) string

Resolve calls the underlying function.

type Response

type Response struct {
	Code    int    `json:"code"`
	Message string `json:"msg"`
	Data    any    `json:"data"`
}

Response is the standard JSON success envelope.

{"code": 200, "msg": "success", "data": ...}

func Success

func Success(data any) *Response

Success builds a success Response envelope.

func SuccessMsg

func SuccessMsg(msg string, data any) *Response

SuccessMsg builds a success Response with a custom message.

type StaticResolver

type StaticResolver struct {
	Messages map[string]string
}

StaticResolver is a simple map-backed resolver. It is useful for testing and for applications that do not need full i18n support.

Missing keys fall back to the key itself. If the resolved template contains format verbs and args are provided, fmt.Sprintf is applied.

func (*StaticResolver) Resolve

func (r *StaticResolver) Resolve(key string, args ...any) string

Resolve looks up the key in the map and applies fmt.Sprintf if args are provided.

Directories

Path Synopsis
gin module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL