“包装错误”在 Go 里是一条用 Unwrap 串起来的链。这篇用真实输出验证:%w 与 %v 的区别、errors.Is 与 errors.As 怎么沿链查找、自定义 Is 方法、不合法的目标会怎样,最后量出链的长度对查找成本的影响。
w2 = "启动服务: 读取配置: not found";链:
[0] *fmt.wrapError:启动服务: 读取配置: not found
[1] *fmt.wrapError:读取配置: not found
[2] *errors.errorString:not found
w2 == ErrNotFound → false;errors.Is(w2, ErrNotFound) → true;errors.Unwrap(w2) == w1 → true
用 %v 而不是 %w:errors.Is(v, ErrNotFound) → false;errors.Unwrap(v) → <nil>;类型 *errors.errorString
一个 Errorf 里两个 %w:类型 *fmt.wrapErrors;errors.Unwrap → <nil>;Is(io.EOF) → true;Is(ErrNotFound) → true%w 让 fmt.Errorf 返回一个带 Unwrap 方法的包装错误,%v 只是把文字拼进去,原来的错误信息保留了但身份丢了。errors.Is 沿 Unwrap 链(多个 %w 时是一棵树)逐个比较。一个 Errorf 里写两个 %w 得到的类型是 *fmt.wrapErrors,它的 Unwrap 返回的是切片,所以 errors.Unwrap 得到 nil,但 errors.Is 能找到两个。
errors.As(err, &target *MyErr) → true;target.Code = 42
os.Open 失败:errors.Is(err, fs.ErrNotExist) → true;errors.As(err, &*fs.PathError) → true,Op = "open",Path = "/no/such/file.txt"
自定义 Is:errors.Is(fmt.Errorf("x: %w", CodeErr{404}), CodeErr{404}) → true;换成 CodeErr{500} → false
不可比较的错误类型 SliceErr(含切片):errors.Is(se, se) → false(没有 panic)
errors.As(err, nil) → panic:errors: target cannot be nil
errors.As(err, target)(目标不是指针的指针) → panic:errors: target must be a non-nil pointer
errors.As(err, &str)(*string 没有实现 error) → panic:errors: *target must be interface or implement error
errors.AsType[*MyErr](err)(泛型版本)→ Code = 42errors.As 要一个“指向目标类型变量的指针”,沿链找到第一个类型匹配的错误并赋给它,标准库的 os.Open 错误就是这样取到 *fs.PathError 里的路径。错误类型可以实现自己的 Is 方法定义“相等”的含义(上面的 CodeErr 按错误码匹配)。目标不合法时 errors.As 直接 panic,消息里写明原因。Go 1.27.1 里还有泛型的 errors.AsType[T],不需要事先声明变量(这次只验证了最基本的用法)。
errors.Is 链长 1(命中最内层) 中位 6.60 ns/op(最小 6.58 / 最大 6.64)
errors.Is 链长 10(命中最内层) 中位 31.36 ns/op(最小 31.28 / 最大 31.54)
errors.Is 链长 50(命中最内层) 中位 145.48 ns/op(最小 143.53 / 最大 147.03)
errors.Is 链长 10(不命中) 中位 36.08 ns/op(最小 35.96 / 最大 36.68)
AllocsPerRun:fmt.Errorf("x: %w", base) 2 次;errors.New 的错误再 Is 0 次;fmt.Errorf("x: %v", base) 2 次每多一层链,errors.Is 多花约 2.8 ns(链长 10 是 31 ns,链长 50 是 145 ns);errors.Is 自己不分配内存。包装本身(fmt.Errorf)要分配 2 次,所以热路径里不要每次都包装一遍再丢掉。