Go errors.As 为什么要求传入指针的指针
来源:17golang原创
时间:2026-11-02 03:42:23 469浏览 收藏
很多人第一次写 errors.As 时,都会在这一行停住:明明想拿到一个 *MyError,为什么不能把它直接传进去,而要写成 &target?关键不在于 errors.As “偏爱指针的指针”,而在于它要把匹配到的错误值写回调用方的变量。
如果目标变量本身的类型是 *MyError,那么把这个变量的地址传给函数后,参数的动态类型自然就是 **MyError。第一颗指针让函数找到变量,第二颗指针只是目标变量本身的类型。理解这层关系,就能同时看懂正确调用、panic 和返回 false。

先看清 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 时,不要把所有失败都归结为“类型没找到”。官方契约把参数形状和匹配结果分开了:
| 现象 | 通常原因 | 排查方向 |
|---|---|---|
| panic | target 是 nil、不是指针,或指向的类型不满足要求 | 检查是否传了变量地址,以及目标类型是否实现 error 或是接口 |
| 返回 false | 参数合法,但错误树中没有可赋值给目标类型的错误 | 检查是否使用 %w 包装、目标类型是否写错、错误是否来自另一条分支 |
| 返回 true 但字段为空 | 自定义错误的字段本身为空,或自定义 As 方法写入了不同值 | 检查错误构造和自定义匹配逻辑,不要只看布尔值 |

例如下面的代码传入了一个错误值,而不是接收结果的地址:
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 却没有正确写入目标,调用方得到的状态就会不符合预期,因此自定义匹配要和目标类型约定一起维护。
一份可复用的排查清单
- 先写出你想读取的错误类型:是
T、*T,还是某个错误接口。 - 声明同类型的接收变量:目标是
*T时写var target *T。 - 把接收变量的地址传入:通常是
errors.As(err, &target)。 - 看到 panic,先查 target 是否为非空指针、指向类型是否满足 errors.As 的契约。
- 看到 false,再查错误是否通过
%w或errors.Join保留在错误树中。 - 匹配成功后再读取字段;不要在布尔结果为 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 取出对应类型。
-
198 收藏
-
218 收藏
-
337 收藏
-
269 收藏
-
341 收藏
-
123 收藏
-
472 收藏
-
149 收藏
-
251 收藏
-
322 收藏
-
448 收藏
-
396 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习