Go 的 error 接口不能直接带错误码,因其仅定义 Error() 方法、无结构化字段;需自定义类型(如含 Code() 方法的结构体),并通过 errors.As() 安全提取嵌套错误中的码。
为什么 Go 的 error 接口不能直接带错误码
go 标准库的 error 是个只含 error() 方法的空接口,它不提供结构化字段,所以你不能直接从任意 error 值里“取”出错误码。想提取错误码,必须让错误类型自己暴露可访问的字段或方法——这意味着得自定义类型,且要约定好提取方式。
常见误区是把错误码塞进 Error() 返回的字符串里再用正则解析,这既脆弱又低效,还破坏错误的可比性和可测试性。
- 错误码应作为结构体字段或方法返回,而非字符串拼接
- 提取时优先用类型断言或
errors.As(),避免字符串匹配 - 如果错误可能被多层包装(比如用
fmt.Errorf("xxx: %w", err)),需用errors.Unwrap()或errors.Is()配合判断
定义带错误码的自定义错误类型
最常用的是结构体实现 error 接口,并加一个 Code() 方法返回码。注意字段名小写(如 code)+ 方法名大写(如 Code()),既保护内部状态,又提供稳定访问入口。
type AppError struct {
code int
msg string
}
func (e *AppError) Error() string { return e.msg }
func (e *AppError) Code() int { return e.code }
var (
ErrNotFound = &AppError{code: 404, msg: "not found"}
ErrConflict = &AppError{code: 409, msg: "conflict"}
)
这样定义后,调用方能安全地用类型断言提取:
if e, ok := err.(*AppError); ok {
log.Printf("error code: %d", e.Code())
}
用 errors.As() 提取嵌套错误中的错误码
实际项目中错误常被包装多次(比如中间件、重试逻辑),直接类型断言会失败。这时必须用 errors.As() 向下查找第一个匹配的底层错误。
-
errors.As(err, &target)会递归解包err,直到找到能赋值给target类型的实例 -
target必须是指针变量,且类型需与自定义错误一致(如*AppError) - 不要用
errors.As(err, new(*AppError))—— 这会产生新指针,无法正确赋值
正确写法:
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.Code() {
case 404:
// 处理未找到
case 409:
// 处理冲突
}
}
错误码设计的几个实际约束
错误码不是越细越好。在 Go 中,错误码本质是“分类标识”,不是 HTTP 状态码的复刻。过度细分会导致类型爆炸和维护困难。
- 建议按业务域分组(如
AuthCodeInvalid=1001,AuthCodeExpired=1002),而非每个错误都独立编号 - 避免用字符串做错误码(如
"auth.invalid"),虽然方便序列化,但丧失编译期检查和 IDE 跳转能力 - 如果错误需要透传到前端,应在 handler 层统一映射为 JSON 响应结构,而不是让下游直接读取
Code()值 - 所有自定义错误类型最好实现
Unwrap()方法(返回nil或包装的 error),否则errors.As()可能无法穿透
真正难的不是定义错误类型,而是让整个调用链都遵守同一套解包和提取规则——一旦某处用 fmt.Errorf("%v", err) 替代 %w,错误码就彻底丢失了。


















