登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go errors.As 为什么要求传入指针的指针

来源:17golang原创

时间:2026-11-02 03:42:23 469浏览 收藏

很多人第一次写 errors.As 时,都会在这一行停住:明明想拿到一个 *MyError,为什么不能把它直接传进去,而要写成 &target?关键不在于 errors.As “偏爱指针的指针”,而在于它要把匹配到的错误值写回调用方的变量。

如果目标变量本身的类型是 *MyError,那么把这个变量的地址传给函数后,参数的动态类型自然就是 **MyError。第一颗指针让函数找到变量,第二颗指针只是目标变量本身的类型。理解这层关系,就能同时看懂正确调用、panic 和返回 false。

包装错误、目标类型、MyError 指针和 errors.As 写回位置的静态关系说明图
图1:errors.As 的 target 类型关系说明图,目标变量是 *MyError 时,传入 &target 才能让函数写回匹配到的指针。

先看清 errors.As 的 target 到底是什么

errors.As 的签名是 func As(err error, target any) bool。第一个参数是要检查的错误,第二个参数不是“类型名称”,而是一个可以被函数修改的目标位置。函数会在错误树中查找可赋值给目标类型的错误;找到后,把那个错误值写入 target 指向的位置,并返回 true。

下面的例子先用 %w 保留错误包装,再把最终的 *MyError 交给调用方:

package main

import (
    "errors"
    "fmt"
)

// MyError 保存调用方真正关心的结构化错误信息。
type MyError struct {
    Code int
    Msg  string
}

// Error 让 MyError 满足 error 接口。
func (e *MyError) Error() string {
    return fmt.Sprintf("code=%d: %s", e.Code, e.Msg)
}

func loadConfig() error {
    // %w 保留底层错误,errors.As 才能继续沿错误树查找。
    return fmt.Errorf("读取配置失败: %w", &MyError{Code: 1001, Msg: "字段缺失"})
}

func main() {
    err := loadConfig()

    var target *MyError
    // target 是接收结果的变量;&target 才是它的写入位置。
    if errors.As(err, &target) {
        fmt.Println(target.Code, target.Msg)
    }
}

这里的关系可以拆成三层:错误树里实际存在的是一个 *MyError;调用方声明的变量 target 也是 *MyError;为了让函数修改这个变量,调用时把它的地址 &target 传进去,于是 target 参数接收到的是 **MyError。

为什么 *MyError 要写成 **MyError

把上面的变量换成类型符号更直观。设匹配到的错误类型为 E:

  • 如果 E 是 *MyError,接收结果的变量就是 var target *MyError。
  • 函数要修改 target 这个变量,必须拿到它的地址:&target。
  • &target 的类型就是 **MyError,也就是“指向一个 *MyError 变量的指针”。

因此,所谓“指针的指针”描述的是传入参数的形状,不是说错误对象必须再套一层指针。errors.As 需要的是一个非空指针,指向它可以写入的目标类型;目标类型刚好是指针时,外层写入指针就会让类型看起来变成二级指针。

这也解释了常见的错误写法:

var target *MyError

// 错误:target 还是 nil,传入的不是 target 变量的地址。
// errors.As(err, target)

// 正确:让 errors.As 能把匹配到的 *MyError 写回 target。
ok := errors.As(err, &target)

直接传 target 时,接口里装的是一个 nil 的 *MyError,它不是可写入的非空目标指针,通常会触发 panic。取地址后,&target 指向一个真实存在的变量,函数才有位置存放匹配结果。

接口目标和普通值目标不都需要“指针的指针”

“errors.As 一定要传二级指针”是不准确的。应该先问:接收结果的变量本身是什么类型?如果接收的是接口变量,它通常只需要传接口变量的地址:

type Temporary interface {
    error
    Temporary() bool
}

var target Temporary
// target 是接口变量,&target 是 *Temporary,不是 **MyError。
if errors.As(err, &target) {
    fmt.Println("临时错误:", target.Error())
}

如果目标类型是一个实现了 error 的值类型,变量也可能是值类型:

type CodeError struct {
    Code int
}

// 值接收者让 CodeError 本身实现 error。
func (e CodeError) Error() string { return fmt.Sprint(e.Code) }

var target CodeError
// 目标变量是 CodeError,因此它的地址是 *CodeError。
if errors.As(err, &target) {
    fmt.Println(target.Code)
}

不过工程代码里更常见的是带字段的指针错误,例如 *os.PathError、*url.Error 或自定义的 *MyError。这类场景下,声明 var target *T,再传 &target,就会自然出现 **T。

panic 和 false 是两类完全不同的问题

排查 errors.As 时,不要把所有失败都归结为“类型没找到”。官方契约把参数形状和匹配结果分开了:

现象通常原因排查方向
panictarget 是 nil、不是指针,或指向的类型不满足要求检查是否传了变量地址,以及目标类型是否实现 error 或是接口
返回 false参数合法,但错误树中没有可赋值给目标类型的错误检查是否使用 %w 包装、目标类型是否写错、错误是否来自另一条分支
返回 true 但字段为空自定义错误的字段本身为空,或自定义 As 方法写入了不同值检查错误构造和自定义匹配逻辑,不要只看布尔值
errors.As 的 target 合法性、错误树遍历、匹配结果和调用方变量的静态边界说明图
图2:errors.As 参数边界说明图,panic 通常来自 target 形状不合法,false 则表示错误树中没有可赋值的匹配值。

例如下面的代码传入了一个错误值,而不是接收结果的地址:

var target *MyError

// 这里把错误值当成 target 传入,目标形状不合法,不能用于写回。
// errors.As(err, *target)

// 这里才是可写的接收位置。
if !errors.As(err, &target) {
    // 参数合法但没有匹配时,选择保留原始错误上下文。
    return fmt.Errorf("读取配置失败,未识别错误类型: %w", err)
}

另外,错误树是否连通取决于包装方式。fmt.Errorf("...: %w", err) 会保留单个被包装错误;errors.Join 会保留多个子错误。若只是把原错误格式化成字符串,匹配类型就丢失了,errors.As 只能对当前错误值和仍然可遍历的包装链工作。

自定义 As 方法仍然遵守“写回位置”

错误类型可以实现 As(any) bool 来定义自定义匹配。这个方法收到的参数仍然是调用方传进来的目标位置,所以实现者要把结果写入它,而不是只返回 true:

type AliasError struct {
    Message string
}

func (e AliasError) Error() string { return e.Message }

func (e AliasError) As(target any) bool {
    // 只有调用方请求 *MyError 时,才负责向 **MyError 写入结果。
    dst, ok := target.(**MyError)
    if !ok {
        return false
    }
    *dst = &MyError{Code: 2001, Msg: e.Message}
    return true
}

这里的 **MyError 就不是偶然出现的:调用方的变量是 *MyError,自定义 As 收到的是这个变量的地址。若方法返回 true 却没有正确写入目标,调用方得到的状态就会不符合预期,因此自定义匹配要和目标类型约定一起维护。

一份可复用的排查清单

  1. 先写出你想读取的错误类型:是 T、*T,还是某个错误接口。
  2. 声明同类型的接收变量:目标是 *T 时写 var target *T。
  3. 把接收变量的地址传入:通常是 errors.As(err, &target)。
  4. 看到 panic,先查 target 是否为非空指针、指向类型是否满足 errors.As 的契约。
  5. 看到 false,再查错误是否通过 %w 或 errors.Join 保留在错误树中。
  6. 匹配成功后再读取字段;不要在布尔结果为 false 时直接解引用可能仍为 nil 的指针。
一句话总结:errors.As 不是要求错误对象必须是“指针的指针”,而是要求传入一个可写的目标位置。目标变量若是 *MyError,取它的地址后自然得到 **MyError;目标若是接口或值类型,指针层级则随接收变量的实际类型变化。

相关问题

errors.As 和类型断言有什么区别?

类型断言只检查当前接口里装的值,errors.As 还会沿 Unwrap 错误树查找,所以适合处理保留上下文的包装错误。

为什么 errors.As 返回 true 但 target 仍然是 nil?

通常应先检查是否实现了自定义 As(any) bool,以及它是否真的向传入的目标位置写入了结果;标准匹配会把可赋值的错误写回 target。

只想判断错误类别时还要用 errors.As 吗?

如果只关心哨兵错误是否存在,优先考虑 errors.Is;只有需要读取结构化字段或调用特定方法时,才用 errors.As 取出对应类型。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>