go 1.13 之前,errors.new 和 fmt.errorf 仅支持字符串拼接,无法嵌套错误。

Go 1.13 之前:errors.New 和 fmt.Errorf 只能拼字符串,无法嵌套
旧版 Go(fmt.Errorf("failed to read %s: %v", path, err) 包了一层,err 的原始类型和上下文就丢了——没法判断是不是 os.IsNotExist(err),也没法取底层的 os.PathError 字段。
常见错误现象:if os.IsNotExist(err) 返回 false,即使原始错误确实是文件不存在;或者想重试网络请求时,发现错误里没有可识别的超时标记。
- 所有错误都扁平化为
error接口,没结构、没因果链 -
fmt.Errorf的格式化结果是新字符串,旧错误对象被丢弃,不是“包装”而是“覆盖” - 自定义错误类型必须手动实现
Unwrap()才能参与后续判断,但标准库不认
Go 1.13 引入 errors.Is 和 errors.As:让错误判断从字符串匹配变成类型/值匹配
这两个函数是真正改变游戏规则的点。它们依赖错误链(error chain),而链的建立靠的是 fmt.Errorf("%w", err) 中的 %w 动词——它把原错误作为字段嵌入新错误,而不是转成字符串。
使用场景:HTTP handler 中捕获数据库错误,想区分是连接失败(该重试)还是主键冲突(该返回 409);CLI 工具里解析配置失败,要判断是文件权限问题还是 YAML 语法错。
立即学习“go语言免费学习笔记(深入)”;
-
errors.Is(err, os.ErrNotExist)能穿透多层%w包装,找到最内层匹配项 -
errors.As(err, &pathErr)可以把任意深度包装的*os.PathError提取出来,访问pathErr.Path或pathErr.Err - 注意:
%w只接受单个error类型参数,不能写fmt.Errorf("x: %w, y: %w", err1, err2)—— 会编译报错
Go 1.20+ 的 fmt.Errorf 默认支持多层包装:但别滥用 %w
Go 1.20 允许 fmt.Errorf 在一个调用里多次用 %w,比如 fmt.Errorf("read %s and %s failed: %w, %w", a, b, errA, errB)。但这只是语法糖,底层仍是单向链表,errors.Is 只会线性遍历第一个 %w 分支,第二个会被忽略。
性能影响:每次 %w 都新建一个错误对象,链越长,errors.Is 和 errors.As 遍历越慢;如果错误链超过几十层,可能触发栈检测或 GC 压力。
- 真正需要并列多个底层错误时,应该自定义结构体实现
Unwrap() []error(Go 1.20+ 支持),而不是堆%w -
%w不等于日志记录——别为了“留痕”在每层都包一次,只在语义上有新上下文时才用(例如:“加载配置” → “读取 config.yaml” → “打开文件”) - 第三方库若未升级到支持
%w,它的错误仍无法被errors.Is穿透,得手动检查err.Error()或用strings.Contains
跨服务错误传播:HTTP API 返回错误时,别把原始错误直接暴露给前端
后端内部用 %w 包装很合理,但对外 HTTP 响应必须脱敏。Go 标准库的 http.Error 或 json.Marshal 都不会自动展开错误链,如果你直接把 err 写进 JSON response,前端看到的只是最外层消息,且可能含敏感路径或堆栈。
容易踩的坑:return JSON{"error": err.Error()} —— 这既丢失了错误码,又泄露了内部细节;或者用 errors.Unwrap(err) 只取一层,误判根本原因。
- 对外错误建议统一用自定义 error struct,实现
Error()返回用户友好的消息,StatusCode()返回 HTTP 状态码,再通过中间件注入X-Request-ID方便查日志 - 调试时可用
fmt.Printf("%+v", err)查看完整错误链(需用%+v,普通%v只打最外层) - gRPC 场景下,
status.FromError(err)能自动识别status.Status类型,但对普通%w包装的错误无效,仍需手动转换










