Golang如何实现链式错误处理_使用fmt.Errorf包装原始错误

使用%w而非%v包装错误可保留原始错误类型和堆栈,使errors.Is()和errors.As()有效;%w要求参数为error类型,空值需提前判空,否则导致nil链或潜在panic。

fmt.Errorf 包装错误时为什么原始错误丢失了

直接用 fmt.Errorf("something went wrong: %v", err) 会丢掉原始错误的类型和堆栈,因为 %v 只调用 err.Error(),返回纯字符串。下游无法用 errors.Is()errors.As() 判断或提取原始错误。

必须用 %w 才能保留错误链

%w 是 Go 1.13 引入的动词,专用于包装错误并保留底层错误的可检查性。它要求参数是 error 类型,否则编译报错:cannot use ... as error value in argument to fmt.Errorf: missing method Error

  • 正确写法:
    err := fmt.Errorf("failed to open config: %w", os.Open("config.json"))
  • 错误写法(丢失链):
    err := fmt.Errorf("failed to open config: %v", os.Open("config.json"))
  • 嵌套多层也有效:
    err := fmt.Errorf("processing failed: %w", fmt.Errorf("validation error: %w", validationErr))

链式错误的实际判断与提取场景

%w 包装后,才能在上层做语义化错误处理。比如重试逻辑只对网络超时重试,忽略权限错误;或日志中只打印最外层消息,但调试时展开全部原因。

  • 判断是否为某类错误:
    if errors.Is(err, os.ErrNotExist) { /* 处理文件不存在 */ }
  • 提取具体错误类型:
    var pathErr *os.PathError
    if errors.As(err, &pathErr) { /* 获取路径、操作等细节 */ }
  • 打印完整错误链(Go 1.20+):
    fmt.Printf("%+v\n", err) // 显示每一层 + 堆栈

注意 wrap 后的错误类型和 nil 行为

fmt.Errorf(... %w) 返回的仍是 *fmt.wrapError 类型,不是原错误类型;且只要任一环节返回 nil,整个链就变成 nil —— 这点容易被忽略。

  • 避免空包装:
    if err != nil {
        return fmt.Errorf("read header: %w", err)
    }

    return nil // 不要写成 fmt.Errorf("read header: %w", nil)
  • 包装前务必判空,否则 %w 遇到 nil 会 panic(实际不会 panic,但结果是 fmt.Errorf("msg: %w", nil) 返回 nil,可能引发空指针后续问题)
  • 自定义错误类型实现 Unwrap() error 也能参与链式判断,但多数情况直接用 %w 更轻量

链式错误不是加几层 %w 就完事,关键在每层包装都得有明确语义,且下游真会用 Is/As 做分支处理;否则只是徒增堆栈深度。