Go errors.As 为什么要求传入指针的指针
很多人第一次写 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 取出对应类型。
Go http.Cookie.Valid 为什么会拒绝不合法的 SameSite 值
- 上一篇
- Go http.Cookie.Valid 为什么会拒绝不合法的 SameSite 值
- 下一篇
- Go tls.Conn.HandshakeContext 取消后怎样判断连接是否还能复用
-
- Golang · Go问答 | 24分钟前 |
- Go work sync 为什么不会自动改写 go.mod
- 198浏览 收藏
-
- Golang · Go问答 | 57分钟前 |
- Go reflect.TypeFor 泛型代码获取反射类型的实现边界
- 218浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go range over function 为什么不能复用旧版编译器
- 269浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · Go go/ast PreorderStack 语法树
- Go ast.PreorderStack 怎样获取语法树的祖先节点
- 341浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go comparable 约束为什么拒绝 interface 类型
- 123浏览 收藏
-
- Golang · Go问答 | 1小时前 | 并发 · 标准库 · go · 性能 · 运行时 · Go 性能排查 运行时指标 runtime/metrics.Read metrics.Sample
- Go runtime/metrics.Read 怎样复用样本缓冲区
- 472浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go unsafe.Pointer 转换为什么可能让 GC 失去追踪
- 149浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go http.Request.Cookie 未找到时怎样判断缺失和空值
- 251浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go defer 参数为什么在 defer 语句执行时就求值
- 322浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 838次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 835次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 919次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 894次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 718次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go代码规范错误处理示例经验总结
- 2022-12-23 278浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览

