go错误是值,需显式处理;自定义错误结构体可携带码、上下文并支持errors.is/as判断;避免字符串比较和过度包装,重在语义化错误策略。

Go 语言没有传统意义上的“异常”,错误是值,必须显式处理;自定义错误类型不是为了模仿 try/catch,而是为了更精确地表达失败语义、携带上下文、支持程序逻辑分支判断。
用 errors.New 和 fmt.Errorf 足够吗?
够用,但不够好。它们返回的是 *errors.errorString 或 *errors.fmtError,无法区分错误种类,也无法附加结构化字段(如错误码、请求 ID、重试建议)。
-
errors.New("timeout")无法告诉调用方这是网络超时还是数据库查询超时 -
fmt.Errorf("failed to parse %s: %w", input, err)虽支持包装,但依然缺失业务标识和可编程判断能力 - 日志中只看到字符串,无法做
if errors.Is(err, ErrNotFound)这类语义判断
定义带错误码的自定义错误结构体
推荐用结构体封装,嵌入 error 接口并实现 Error() 方法,同时导出可比较的变量或类型用于判断。
type AppError struct {
Code int
Message string
ReqID string
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}
return e.Message
}
func (e *AppError) Unwrap() error { return e.Cause }
var (
ErrNotFound = &AppError{Code: 404, Message: "not found"}
ErrInvalid = &AppError{Code: 400, Message: "invalid request"}
)
- 导出
ErrNotFound这类变量,便于在调用处用errors.Is(err, ErrNotFound)判断 - 实现
Unwrap()后,errors.Is和errors.As才能穿透包装链 - 避免用
string字段做类型判断(如err.Error() == "not found"),不可靠且易破
用 errors.As 提取结构化错误信息
当错误被多层包装(比如 fmt.Errorf("handling: %w", appErr)),需要用 errors.As 安全提取原始结构体,而不是类型断言。
立即学习“go语言免费学习笔记(深入)”;
err := doSomething()
var appErr *AppError
if errors.As(err, &appErr) {
log.Printf("code=%d req_id=%s: %s", appErr.Code, appErr.ReqID, appErr.Message)
switch appErr.Code {
case 404:
// 返回 404 响应
case 500:
// 触发告警
}
}
-
errors.As会递归检查整个错误链,比err.(*AppError)更健壮 - 注意传入的是指针地址
&appErr,否则无法赋值 - 如果只是判断类型存在,不用读字段,优先用
errors.Is+ 导出变量,性能更好
别忽略错误包装的代价与可读性权衡
每层 %w 都会增加内存分配和栈追踪开销,过度包装会让错误消息冗长难读。
- 内部函数返回底层错误时,不要无脑
fmt.Errorf("xxx: %w", err)—— 先判断是否需要保留原始原因 - 对外暴露的错误,应聚合关键信息,而非堆砌调用栈(Go 默认不自动捕获栈,这点和 Java 不同)
- 调试阶段可临时加
fmt.Errorf("%w %+v", err, debug.Stack()),但上线前删掉
真正难的不是定义一个 type MyError struct,而是决定哪些错误值得结构化、哪些该被吞掉、哪些要向上透传——这取决于你的错误策略,而不是语法能力。










