Golang中errors.As方法的第二个参数详解
Golang中errors.As方法的第二个参数详解
为什么 errors.As 方法的第二个参数需要是指向指针的指针?
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
当我尝试只使用指向错误类型的指针时,从 gopls 我收到 errors.As 的第二个参数必须是一个非空指针,指向实现了 error 的类型,或者指向任何接口类型。我不太理解这个错误信息。
更多关于Golang中errors.As方法的第二个参数详解的实战教程也可以访问 https://www.itying.com/category-94-b0.html
5 回复
抱歉,你能再解释一下吗?我还是没明白。
你好。你传递的是一个值的指针,因为根据文档,As 函数需要断言错误的类型并将其赋值给传入的值:
// As 在 err 的树中找到第一个与 target 匹配的错误,如果找到,则将 target 设置为该错误值并返回 true。否则,返回 false。
errors.As 方法的第二个参数需要是指向指针的指针,这是由 Go 语言的类型系统和 errors.As 的设计目标共同决定的。让我通过代码示例来详细解释:
核心原因
errors.As 需要修改调用者传入的变量,使其指向匹配的错误值。由于 Go 是值传递语言,要修改外部变量必须传递指针。
示例分析
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
// 场景1:错误的用法 - 只传递指针
var pathError *fs.PathError
// 如果这样调用:errors.As(err, pathError)
// 编译器会报错,因为无法修改 pathError 指向的内容
// 场景2:正确的用法 - 传递指针的指针
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError // 这是一个指针变量
if errors.As(err, &pathError) { // 传递指针的地址
// 此时 pathError 被修改为指向 err 中的 *fs.PathError
fmt.Printf("Type: %T, Path: %s\n", pathError, pathError.Path)
}
}
// 场景3:接口类型的示例
var err error
if _, err := os.Open("non-existing"); err != nil {
var targetErr *fs.PathError
// errors.As 内部会做类似这样的操作:
// *targetErrPtr = concreteError.(*fs.PathError)
// 其中 targetErrPtr 是 &targetErr
if errors.As(err, &targetErr) {
fmt.Println("Matched PathError:", targetErr.Path)
}
}
// 场景4:更清晰的对比
demoErrorsAs()
}
func demoErrorsAs() {
// 模拟一个多层包装的错误
err := &fs.PathError{Op: "open", Path: "/tmp/test", Err: os.ErrNotExist}
wrappedErr := fmt.Errorf("context: %w", err)
// 正确:使用指针的指针
var pe *fs.PathError
if errors.As(wrappedErr, &pe) {
fmt.Printf("Success: pe now points to the PathError, Path=%s\n", pe.Path)
}
// 错误:如果只传递指针值会发生什么
var pe2 *fs.PathError
// 假设 errors.As 签名是 func As(err error, target *error) bool
// 那么调用 errors.As(wrappedErr, pe2) 时:
// 1. pe2 是 nil(零值)
// 2. 函数内部无法修改 pe2 使其指向实际的错误
// 3. 调用者永远无法获取匹配到的错误值
}
底层实现原理
查看 errors.As 的简化实现可以帮助理解:
// 简化的实现逻辑
func As(err error, target interface{}) bool {
// target 必须是指针类型
// 并且指向的类型必须是 error 接口或实现了 error 的具体类型
// 内部会通过反射检查并设置值:
// val := reflect.ValueOf(target)
// if val.Kind() != reflect.Ptr || val.IsNil() {
// panic("target must be a non-nil pointer")
// }
// 遍历错误链
for err != nil {
// 如果 target 是指向接口的指针
if reflect.TypeOf(err).AssignableTo(targetType) {
// 这里需要修改 target 指向的值
// 所以 target 本身必须是指针
setValue(target, err)
return true
}
// 检查错误链
if x, ok := err.(interface{ Unwrap() error }); ok {
err = x.Unwrap()
} else {
break
}
}
return false
}
更多示例
package main
import (
"errors"
"fmt"
)
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("code %d: %s", e.Code, e.Message)
}
func main() {
// 示例1:具体错误类型
err := &MyError{Code: 404, Message: "Not Found"}
wrapped := fmt.Errorf("wrapped: %w", err)
var myErr *MyError
if errors.As(wrapped, &myErr) {
fmt.Printf("Code: %d, Message: %s\n", myErr.Code, myErr.Message)
}
// 示例2:接口类型
var errInterface error
nestedErr := fmt.Errorf("level2: %w",
fmt.Errorf("level1: %w",
&MyError{Code: 500, Message: "Internal Error"}))
if errors.As(nestedErr, &errInterface) {
fmt.Printf("Error: %v\n", errInterface)
}
// 示例3:为什么需要指针的指针 - 对比实验
compareApproaches()
}
func compareApproaches() {
fmt.Println("\n--- 对比实验 ---")
err := &MyError{Code: 100, Message: "Test"}
// 方法A:正确的方式
var targetA *MyError
if errors.As(err, &targetA) {
fmt.Printf("A: Success - %v\n", targetA)
}
// 方法B:如果 errors.As 接受指针值而不是指针的指针
// 假设有这样一个函数:
// func AsValue(err error, target *MyError) bool
// 那么调用时:AsValue(err, targetA)
// 问题:函数内部无法让 targetA 指向新的 MyError 实例
// 只能修改 targetA 当前指向的内容,但如果 targetA 是 nil 就无能为力
}
关键点总结
- 修改需求:
errors.As需要修改调用者的变量,使其指向匹配的错误值 - 值传递限制:Go 函数参数是值传递,要修改外部变量必须传递指针
- 指针变量本身:当变量已经是指针类型时,要修改这个指针变量,就需要传递指针的指针
- 类型安全:这种设计确保了类型安全,编译器可以在编译时检查类型匹配
这种设计模式在 Go 标准库中很常见,比如 json.Unmarshal、reflect.Value.Set 等方法都采用类似的方式,通过传递指针来允许函数修改调用者的变量。


