go的error接口不适合直接表达领域错误,因其为空接口,缺乏状态、上下文和分类能力,导致跨层传递时错误识别易断裂,需用自定义domainerror类型配合unwrap和errors.as实现可识别、可分类、可翻译的领域错误处理。

为什么 Go 的 error 接口不适合直接表达领域错误
Go 的 error 是个空接口,没法自带状态、上下文或分类能力。你返回 fmt.Errorf("user not found"),调用方只能靠字符串匹配或类型断言去识别——这在分层架构里极易断裂:HTTP 层想返回 404,业务层却只抛出一个泛化错误,中间还隔着 service、repo 多层包装。
- 常见错误现象:
errors.Is(err, ErrUserNotFound)在跨包传递后失效,因为 repo 包里定义的ErrUserNotFound和 handler 包里的不是同一个变量 - 根本原因:Go 没有“受检异常”,但领域错误需要可识别、可分类、可翻译(比如转成 HTTP 状态码或 i18n 错误消息)
- 正确做法不是塞更多字符串,而是让错误本身携带类型信息和结构化字段
用自定义错误类型 + Unwrap 实现分层透传
领域错误必须能穿透 service → repo → db 各层,同时不丢失原始语义。关键不是“包装”,而是“可解包”和“可判定”。
- 定义统一错误基类:
type DomainError struct { Code string; Message string; Cause error },并实现Unwrap() error返回Cause - 各层只做轻量包装,不覆盖原始
Code:return &DomainError{Code: "USER_NOT_FOUND", Message: "user not exist", Cause: repoErr} - HTTP 层用
errors.As()向下查找具体类型:var de *DomainError; if errors.As(err, &de) { switch de.Code { case "USER_NOT_FOUND": http.Error(w, de.Message, http.StatusNotFound) } } - 避免踩坑:别用
fmt.Errorf("%w", err)直接套壳——它会丢失Code字段;也别在每层都 new 一个新DomainError,否则errors.As找不到最外层的领域错误实例
errors.Is 和 errors.As 在领域错误中的真实分工
errors.Is 适合判断是否属于某类错误(如“是不是权限错误”),errors.As 用于提取结构化信息(如错误码、用户 ID)。两者不能混用。
- 用
errors.Is(err, ErrForbidden)判断是否该拦截请求(适合全局中间件) - 用
errors.As(err, &de)提取de.Code做 HTTP 映射或日志标记(适合 handler 层) - 别写
errors.Is(err, &DomainError{Code: "USER_NOT_FOUND"})——Is比较的是值相等,不是字段匹配 - repo 层返回的原始错误(如
sql.ErrNoRows)应被明确映射为领域错误,而不是留到上层靠Is猜测
HTTP 层如何安全地把领域错误转成响应而不泄露内部细节
领域错误的 Message 是给开发者看的,HTTP 响应体里的提示必须可控。不能直接把 err.Error() 写进 JSON。
立即学习“go语言免费学习笔记(深入)”;
- 定义错误码到 HTTP 状态码的映射表,例如:
map[string]int{"USER_NOT_FOUND": http.StatusNotFound, "VALIDATION_FAILED": http.StatusBadRequest} - 定义错误码到用户提示的映射(支持多语言时用 key,如
"user_not_found"),绝不用DomainError.Message直出 - 敏感字段(如 SQL 报错、路径、参数名)必须从错误链中剥离:检查
errors.Unwrap链,遇到*pq.Error或*mysql.MySQLError就跳过,只保留顶层DomainError - 容易忽略的一点:gRPC 和 HTTP 共享同一套领域错误时,HTTP 层要额外做一次“降级”——gRPC 可以传详细
Status,HTTP 则必须限制响应体大小和字段可见性










