go语言通过自定义错误类型和errors.as/errors.is实现细粒度错误分类,关键在于精准判断与恢复:需定义具名错误类型、用%w包装、按类型返回http状态码,避免字符串匹配或错误链断裂。

Go 语言没有传统意义上的异常继承体系,但可以通过接口、自定义类型和错误包装(errors.As / errors.Is)实现真正可用的细粒度错误分类——关键不在于“抛出时多花哨”,而在于“判断和恢复时是否精准”。
用自定义错误类型区分业务语义
直接用 fmt.Errorf 或 errors.New 生成的错误无法携带结构化信息,也无法被可靠识别。必须定义具名错误类型,并让其实现 error 接口:
type ValidationError struct {
Field string
Value interface{}
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %q: %v", e.Field, e.Value)
}
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}
这样后续就能用 errors.As 精准断言:
-
if errors.As(err, &ValidationError{})→ 捕获所有字段校验失败 -
if errors.As(err, &NotFoundError{})→ 区分资源缺失与系统错误 - 避免用字符串匹配(如
strings.Contains(err.Error(), "not found")),它脆弱且不可维护
用错误包装(Wrap)保留原始上下文,同时支持分类判断
底层函数返回具体错误(如 *ValidationError),上层调用者应使用 fmt.Errorf("xxx: %w", err) 包装,而非 %v 或 %s —— 只有 %w 才能让 errors.As 和 errors.Is 向下穿透:
立即学习“go语言免费学习笔记(深入)”;
func CreateUser(u User) error {
if err := validateUser(u); err != nil {
return fmt.Errorf("create user failed: %w", err) // ✅ 正确包装
}
// ...
}
调用方仍可准确识别原始错误类型:
-
if errors.As(err, &ValidationError{})→ 成功匹配,即使 err 是多层包装后的结果 -
if errors.Is(err, sql.ErrNoRows)→ 也能穿透到最内层的底层错误 - 错用
%v会切断错误链,导致分类失效
在 HTTP handler 中按错误类型返回不同状态码
错误分类的最终价值体现在响应决策上。不要把所有错误都映射成 500:
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
err := h.service.Create(r.Context(), user)
if err != nil {
switch {
case errors.As(err, &ValidationError{}):
http.Error(w, err.Error(), http.StatusBadRequest)
case errors.As(err, &NotFoundError{}):
http.Error(w, err.Error(), http.StatusNotFound)
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "request timeout", http.StatusGatewayTimeout)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
// ...
}
注意:context.DeadlineExceeded 是预定义变量,不是字符串;errors.Is 对它有效,但 errors.As 不适用(它不是指针类型)。
避免常见反模式:过度嵌套、忽略包装、混用 error 类型
细粒度分类失效,往往不是因为没写类型,而是因为用法松散:
- 同一业务域的错误类型应统一放在一个包里(如
pkg/errors),避免散落在各处 - 不要在中间层反复
fmt.Errorf("%w")包装同一错误多次——这会让堆栈冗长且无意义 - 不要把
os.PathError、net.OpError等标准错误直接暴露给上层业务逻辑;必要时用errors.As提取并转为领域错误(如&FileAccessError{Path: ...}) - 日志中打印错误时,优先用
%+v(来自github.com/pkg/errors或 Go 1.13+ 的默认格式),才能看到完整错误链
最难的不是定义错误类型,而是坚持在每一层都做一致的错误识别与转换——漏掉一层,分类就断了。










