Go语言需自定义模块级错误类型而非裸int码,如user.Error结构体含Code/Message/TraceID,配构造函数和StatusCode()方法实现可识别、可分类、可追溯的错误处理。

Go 语言本身没有内置错误码系统,error 是接口,不带结构化状态码;想实现模块级错误码,必须自己封装——但别直接用全局 int 常量或字符串拼接,那会迅速失控。
为什么不能用裸 int 错误码
裸 int(比如 const ErrNotFound = 404)看似简单,实际带来三个硬伤:无法携带上下文、跨模块易冲突、调用方无法安全类型断言。更糟的是,一旦模块升级新增错误码,调用方可能因未处理新码而 panic 或静默失败。
- 所有错误码必须绑定到具体模块类型,例如
user.ErrInvalidEmail,而非全局ErrInvalidEmail - 错误类型应实现
error接口且嵌入fmt.Stringer,方便日志和调试 - 避免用
errors.New("user: invalid email")这类纯字符串错误——它无法被程序逻辑识别,只能靠字符串匹配,脆弱且不可维护
如何定义模块级错误类型(以 user 模块为例)
核心是让每个模块拥有自己的错误类型,并支持码值 + 消息 + 可选字段。推荐用结构体而非自定义 error 类型的接口实现,便于扩展字段(如 traceID、HTTP 状态码)。
package user
type ErrorCode int
const (
ErrInvalidEmail ErrorCode = iota + 10000
ErrDuplicateEmail
ErrUserNotFound
)
type Error struct {
Code ErrorCode
Message string
TraceID string // 可选,用于链路追踪
}
func (e *Error) Error() string {
if e.TraceID != "" {
return e.Message + " (trace: " + e.TraceID + ")"
}
return e.Message
}
// WithTrace 返回带 traceID 的新错误实例
func (e *Error) WithTrace(traceID string) *Error {
newErr := *e
newErr.TraceID = traceID
return &newErr
}
// NewErrInvalidEmail 是构造函数,隐藏内部结构
func NewErrInvalidEmail() *Error {
return &Error{Code: ErrInvalidEmail, Message: "email format invalid"}
}
- 错误码从
10000起始,为后续子模块(如user.auth)留出空间 - 构造函数(如
NewErrInvalidEmail)强制调用方使用语义化创建方式,避免手动 new struct - 不导出
Code字段的 setter,防止外部篡改码值
如何在 handler 层统一转 HTTP 状态码
模块错误不等于 HTTP 错误,但多数 Web 场景需要映射。关键不是写一堆 switch,而是让错误类型自己“知道”该返回什么状态。
立即学习“go语言免费学习笔记(深入)”;
- 在错误结构体中增加
StatusCode() int方法,不同错误码返回不同 HTTP 状态,比如ErrUserNotFound.StatusCode()返回404 - handler 中统一用
switch err.(type)判断是否为*user.Error,再调err.StatusCode()和err.Error(),避免散落各处的 map 表 - 不要在错误构造时硬编码
http.StatusNotFound——状态码属于传输层契约,应由 handler 决定,错误类型只提供语义依据
为什么 error.Is / errors.As 不够用
errors.Is 和 errors.As 依赖底层 error 是否是同一实例或实现了特定接口,但模块错误往往经过多层包装(比如 fmt.Errorf("failed to create user: %w", userErr)),原始 *user.Error 被包进 *fmt.wrapError,errors.As 就失效了。
- 解决方案:所有包装都用
fmt.Errorf("%w", err),并确保模块错误类型实现Unwrap() error方法返回nil(表示无更深层错误)或自身(若需链式解包) - 更稳妥的方式是加一个
IsUserError(err error) bool工具函数,用errors.As+ 类型断言双保险 - 永远别依赖错误消息字符串做判断——
strings.Contains(err.Error(), "invalid email")是反模式,一改提示就崩
真正难的不是定义错误码,而是让整个团队遵守“错误必须可识别、可分类、可追溯”的约束。一旦有人开始用 fmt.Errorf("something went wrong") 替代模块错误构造函数,整套体系就从根上松动了。


















