业务错误应使用自定义 error 类型而非 errors.New,因其支持类型判断与结构化字段;系统错误需谨慎使用 %w 单次包装以保留错误链;HTTP handler 中应优先用 errors.Is 和 errors.As 区分错误类型。

业务错误该用自定义 error 类型,而不是 errors.New
业务错误的核心特征是:可预期、需被上层逻辑识别并处理(比如返回特定 HTTP 状态码或重试策略),而 errors.New 生成的只是普通字符串 error,无法携带类型信息或结构化字段。一旦用 errors.New("user not found"),调用方只能靠 strings.Contains 或 error.Error() 匹配字符串——这极易因拼写变动或翻译导致漏判。
正确做法是定义带接口实现的结构体:
type UserNotFoundError struct {
UserID int
}
func (e *UserNotFoundError) Error() string {
return fmt.Sprintf("user not found: id=%d", e.UserID)
}
func (e *UserNotFoundError) Is(target error) bool {
_, ok := target.(*UserNotFoundError)
return ok
}
这样调用方可安全使用 errors.Is(err, &UserNotFoundError{}) 判断,且不依赖字符串内容。
系统错误必须保留原始 error 链,避免用 fmt.Errorf("%w") 外包两次
系统错误(如数据库连接失败、网络超时、文件 I/O 错误)往往需要完整上下文用于排查。Go 1.13+ 的错误链机制依赖 %w 动词单次包装。常见错误是层层用 fmt.Errorf("xxx: %w", err) 套娃,导致错误链过长、关键底层错误被埋没,甚至触发 errors.Unwrap 无限循环。
立即学习“go语言免费学习笔记(深入)”;
应遵循原则:
- 只在必要位置(如跨层边界)用一次
%w包装 - 绝不把已包装过的 error 再用
%w二次包装 - 日志中用
fmt.Printf("%+v", err)查看完整栈和链
例如:
// ✅ 正确:仅在 service 层包装一次
if err := repo.GetUser(ctx, id); err != nil {
return nil, fmt.Errorf("failed to get user from repo: %w", err)
}
// ❌ 错误:handler 层又包一次
if err := svc.GetUser(id); err != nil {
return fmt.Errorf("failed to handle user request: %w", err) // 这会让原始 error 被包两层
}
HTTP handler 中区分两类错误要靠 errors.As 和 errors.Is,不是 switch err.(type)
switch err.(type) 只能匹配 error 接口的具体类型,但业务错误常被中间件或日志工具用 fmt.Errorf 包装过,原始类型丢失。此时 errors.Is(判断是否为某类错误)和 errors.As(提取具体错误实例)才是可靠手段。
典型 handler 模式:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
id := parseID(r)
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
var notFound *UserNotFoundError
if errors.As(err, ¬Found) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
}
// 其他系统错误统一 500
http.Error(w, "internal error", http.StatusInternalServerError)
log.Printf("unexpected error: %+v", err)
return
}
json.NewEncoder(w).Encode(user)
}
注意:errors.As 要传入指针地址(¬Found),否则无法赋值;errors.Is 对标准库错误(如 context.DeadlineExceeded)直接有效,无需自定义 Is 方法。
全局错误码映射表容易忽略 error 类型一致性
有些团队会建一个 map[error]int 映射业务错误到 HTTP 状态码,但若 map key 是 error 接口值(如 map[error]int{&UserNotFoundError{}: 404}),实际运行时因每次 new 出的指针地址不同,查不到对应码。更糟的是,如果 map key 是字符串(err.Error()),又回到字符串匹配的老问题。
安全做法只有两种:
- 用
errors.Is+ 预定义变量(如var ErrUserNotFound = &UserNotFoundError{}),再用 if/else 或 map[*UserNotFoundError]int - 让业务错误类型实现一个
StatusCode() int方法,统一调用err.StatusCode()
后者更灵活,但要注意:系统错误(如 os.PathError)没有该方法,需 fallback 到默认 500。
这类细节在压力测试或并发场景下才暴露——比如多个 goroutine 同时触发同一类业务错误,却因类型判断失效返回了 200。










