Go中错误码和错误消息不应分开返回,而应通过自定义错误类型(如AppError)封装并实现Error()方法,利用errors.Is进行精确匹配,确保语义一致性和结构化处理。

Go 中错误码和错误消息该不该分开返回
不应该在函数签名里同时返回 error 和独立的错误码(如 int 或 string)。Go 的惯用法是让 error 本身携带足够上下文,包括可识别的错误码、原始消息、堆栈(可选)——否则调用方要写大量重复的 switch 去映射错误值到码,且极易漏处理。
真正需要区分的是:错误是否可恢复、是否需被上层决策(如重试/降级)、是否要透传给前端。这些靠错误类型(interface 实现)或错误包装(errors.As/errors.Is)来判断,而不是靠裸数字。
- 用自定义错误类型封装码和消息,例如
type AppError struct { Code int; Msg string },并实现Error() string - 用
fmt.Errorf("xxx: %w", err)包装底层错误,保留原始错误链 - 避免返回
(int, error)这类双值组合,它破坏了 Go 错误处理的一致性
如何定义和识别业务错误码
推荐用 iota 枚举定义错误码常量,并让每个码对应一个全局唯一的 var 错误变量(不是 struct 实例),这样能用 errors.Is(err, ErrUserNotFound) 精确匹配。
例如:
立即学习“go语言免费学习笔记(深入)”;
const (
CodeUserNotFound = iota + 1000
CodeInvalidParam
CodeInternalError
)
var (
ErrUserNotFound = &AppError{Code: CodeUserNotFound, Msg: "user not found"}
ErrInvalidParam = &AppError{Code: CodeInvalidParam, Msg: "invalid request parameter"}
)
- 错误码建议从 1000 起跳,避开 HTTP 状态码和系统错误码范围
- 每个
var错误应是地址固定的指针,确保errors.Is可靠 - 不要在运行时拼接错误码字符串(如
"ERR_" + strconv.Itoa(code)),这无法被errors.Is捕获
HTTP 接口如何返回错误码和消息给前端
HTTP handler 不该直接把 Go 的 error 原样 JSON 输出。需要统一中间件或响应构造函数,将错误转换为标准响应结构,例如 {"code": 1001, "message": "xxx", "data": null}。
关键点:
- 对已知业务错误(如
ErrUserNotFound),映射固定 HTTP 状态码(如 404)和响应code - 对未预期错误(如数据库连接失败),统一返回 500 + 通用错误码(如 5000),且不暴露敏感信息
- 日志中记录完整错误(含堆栈),但响应体只输出用户友好的
Msg - 避免在 handler 里写
if errors.Is(err, xxx) { return JSON{code: 1001} },应抽成统一的WriteErrorResponse(w, err)
为什么不用 errors.New("code=1001 msg=xxx") 拼接错误
这种字符串拼接方式会让错误失去结构化能力:无法用 errors.Is 判断类型,无法提取 Code 字段,无法添加额外字段(如 traceID、requestID),也无法在日志中做结构化解析。
更糟的是,一旦消息文本变更(比如加个空格或标点),strings.Contains(err.Error(), "code=1001") 就会失效。
- 所有业务错误必须是可断言的类型,例如
if e, ok := err.(*AppError); ok { log.Warn("code", e.Code) } - 如果要用字符串匹配做兜底(极不推荐),至少用
fmt.Sprintf("code=%d", e.Code)保证格式稳定 - 第三方库(如
github.com/pkg/errors)已过时,优先用 Go 1.13+ 的errors包原生能力
最易被忽略的一点:错误码的语义一致性。同一个码在不同服务、不同接口中必须代表完全相同的失败原因,否则前端按码做逻辑分支就会出错。这需要团队约定 + 代码审查,不能只靠文档。










