4. 安全错误码定义:iota、HTTP 映射与国际化

在 Go 安全编程中,错误码不仅仅是“错误编号”,更是安全断言与权限验证的第一道防线。一个设计良好的错误码体系需要满足三个核心要求:可枚举(iota 自动递增)、可传输(与 HTTP 状态码映射)、可理解(国际化多语言)。本章将逐一拆解这三部分,并给出可直接用于生产的安全错误码实现。

安全原则:错误码必须 唯一、稳定、不可猜测。使用 iota 自动生成可以避免人工赋值导致的重复或跳号,同时配合 HTTP 状态码映射,让前端/客户端能快速判断错误类型,而无需暴露内部实现细节。

4.1 使用 iota 定义安全错误码

Go 的 iota 是常量生成器,在 const 块中每行自动递增 1。安全错误码通常按模块分组,每组从 1000 开始,预留扩展空间。

package errcode

// 安全错误码枚举
type ErrCode int

const (
    // 通用错误:1000-1999
    ErrSuccess       ErrCode = 1000 + iota // 1000: 成功
    ErrUnknown                              // 1001: 未知错误
    ErrInvalidParam                         // 1002: 参数无效
    ErrUnauthorized                         // 1003: 未授权
    ErrForbidden                            // 1004: 禁止访问
    ErrNotFound                             // 1005: 资源不存在
    ErrInternal                             // 1006: 内部错误

    // 权限验证错误:2000-2999
    ErrTokenExpired    ErrCode = 2000 + iota // 2000: Token 过期
    ErrTokenInvalid                           // 2001: Token 无效
    ErrPermissionDenied                       // 2002: 权限不足
    ErrRoleNotFound                           // 2003: 角色不存在
    ErrResourceForbidden                      // 2004: 资源被禁止

    // 断言错误:3000-3999
    ErrAssertionFailed ErrCode = 3000 + iota // 3000: 断言失败
    ErrTypeMismatch                           // 3001: 类型不匹配
    ErrNilPointer                             // 3002: 空指针
    ErrBoundCheck                             // 3003: 越界检查
)
设计要点:
  • 每组预留 1000 个编号,避免后续插入新错误码时打乱已有映射。
  • 使用 ErrCode 自定义类型,防止与其他 int 常量混淆。
  • 安全相关错误码(2000+、3000+)与通用错误码(1000+)严格分离,便于日志告警分级。

4.2 错误码与 HTTP 状态码映射

安全错误码不能直接暴露给 HTTP 客户端,必须映射为标准的 HTTP 状态码。映射规则遵循“最小信息泄露”原则:内部错误(500)不返回具体原因,权限错误(403/401)不提示缺少哪个角色。

错误码 含义 HTTP 状态码 映射理由
1000 成功 200 OK 标准成功响应
1003 未授权 401 Unauthorized 需要身份认证
1004 禁止访问 403 Forbidden 已认证但无权限
1005 资源不存在 404 Not Found 隐藏资源是否存在
1006 内部错误 500 Internal Server Error 不暴露堆栈细节
2000 Token 过期 401 Unauthorized 触发重新登录
2002 权限不足 403 Forbidden 不提示缺少哪个角色
3000 断言失败 400 Bad Request 客户端输入异常
// HTTPStatus 根据错误码返回对应的 HTTP 状态码
func (e ErrCode) HTTPStatus() int {
    switch {
    case e == ErrSuccess:
        return http.StatusOK
    case e == ErrUnauthorized || e == ErrTokenExpired || e == ErrTokenInvalid:
        return http.StatusUnauthorized
    case e == ErrForbidden || e == ErrPermissionDenied || e == ErrResourceForbidden:
        return http.StatusForbidden
    case e == ErrNotFound:
        return http.StatusNotFound
    case e >= 3000 && e < 4000:
        return http.StatusBadRequest
    default:
        return http.StatusInternalServerError
    }
}
安全警告:不要将内部错误码(如 1006、3002)直接映射为 200 OK 或 4xx,否则客户端可能误判操作成功。始终遵循“失败时默认 500”的安全兜底策略。
错误码与 HTTP 状态码映射流程 错误码 → HTTP 状态码映射流程 错误码输入 映射引擎 HTTP 状态码输出 映射规则: • 1000 (成功) → 200 OK • 1003/2000/2001 (认证失败) → 401 Unauthorized • 1004/2002/2004 (权限不足) → 403 Forbidden • 3000-3999 (断言错误) → 400 Bad Request • 其他 → 500 Internal Server Error (安全兜底)

4.3 错误码国际化

安全错误信息不能直接返回英文堆栈或中文拼音,必须根据客户端语言偏好返回对应的本地化消息。Go 标准库 golang.org/x/text 提供了完善的国际化支持,但生产环境更推荐使用 map + 语言标签 的轻量方案,避免引入过重的依赖。

// 错误码国际化消息
var errMsgMap = map[ErrCode]map[string]string{
    ErrSuccess: {
        "zh-CN": "操作成功",
        "en-US": "Success",
        "ja-JP": "成功",
    },
    ErrUnauthorized: {
        "zh-CN": "未授权,请先登录",
        "en-US": "Unauthorized, please login",
        "ja-JP": "認証が必要です",
    },
    ErrPermissionDenied: {
        "zh-CN": "权限不足,无法执行此操作",
        "en-US": "Permission denied",
        "ja-JP": "権限がありません",
    },
    ErrAssertionFailed: {
        "zh-CN": "断言失败,输入数据异常",
        "en-US": "Assertion failed, invalid input",
        "ja-JP": "アサーション失敗",
    },
}

// Msg 根据语言返回本地化错误消息
func (e ErrCode) Msg(lang string) string {
    if msgs, ok := errMsgMap[e]; ok {
        if msg, ok := msgs[lang]; ok {
            return msg
        }
        // 默认回退到英文
        if msg, ok := msgs["en-US"]; ok {
            return msg
        }
    }
    return "Unknown error"
}
国际化最佳实践:
  • 始终提供 en-US 作为默认回退语言,避免空字符串。
  • 安全错误消息不要包含具体参数(如“用户 ID 123 不存在”),应统一为“资源不存在”。
  • 在 HTTP 响应头中使用 Accept-Language 或 URL 参数 ?lang=zh-CN 传递语言偏好。
错误码国际化多语言映射 错误码国际化多语言映射 ErrCode 1003 ErrUnauthorized zh-CN 未授权,请先登录 en-US Unauthorized, please login ja-JP 認証が必要です 💡 根据 Accept-Language 头部自动选择语言,默认回退 en-US。 安全消息不包含具体参数,避免信息泄露。

4.4 完整的安全错误码响应结构

将错误码、HTTP 状态码、国际化消息整合为一个统一的 JSON 响应结构,确保前后端契约一致。

// APIResponse 统一响应结构
type APIResponse struct {
    Code    ErrCode `json:"code"`              // 业务错误码
    Message string  `json:"message"`           // 国际化消息
    Data    any     `json:"data,omitempty"`    // 数据负载
}

// NewErrorResponse 创建错误响应
func NewErrorResponse(code ErrCode, lang string) *APIResponse {
    return &APIResponse{
        Code:    code,
        Message: code.Msg(lang),
        Data:    nil,
    }
}

// 使用示例
func handlePermissionCheck(w http.ResponseWriter, r *http.Request) {
    lang := r.Header.Get("Accept-Language")
    if lang == "" {
        lang = "en-US"
    }
    // 权限验证失败
    resp := NewErrorResponse(ErrPermissionDenied, lang)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(ErrPermissionDenied.HTTPStatus())
    json.NewEncoder(w).Encode(resp)
}
生产建议:将 ErrCode 实现 json.Marshaler 接口,使其在 JSON 序列化时自动输出整数而非字符串,减少传输体积。同时,在开发环境可以开启“详细模式”,返回错误码对应的内部名称(如 "ErrPermissionDenied")便于调试。

4.5 本章小结

  • iota 定义:按模块分组,每组预留 1000 个编号,使用自定义类型防止混淆。
  • HTTP 映射:遵循最小信息泄露原则,内部错误统一映射 500,权限错误映射 401/403。
  • 国际化:使用 map + 语言标签轻量实现,默认回退 en-US,安全消息不包含具体参数。
  • 响应结构:统一 APIResponse 结构,包含 code、message、data 三个字段,前后端契约清晰。
安全红线:永远不要在错误消息中返回堆栈信息、数据库查询、文件路径或用户敏感数据。错误码是安全断言的第一道闸门,设计时就要假设攻击者会尝试枚举所有错误码来探测系统边界。