在自定义结构错误上应用`errors.Is`和`errors.As`

问题描述 投票:0回答:2
package main

import (
    "errors"
    "fmt"
)

type myError struct{ err error }

func (e myError) Error() string { return e.err.Error() }

func new(msg string, args ...any) error {
    return myError{fmt.Errorf(msg, args...)}
}

func (e myError) Unwrap() error        { return e.err }
func (e myError) As(target any) bool   { return errors.As(e.err, target) }
func (e myError) Is(target error) bool { return errors.Is(e.err, target) }

func isMyError(err error) bool {
    target := new("")
    return errors.Is(err, target)
}

func asMyError(err error) (error, bool) {
    var target myError
    ok := errors.As(err, &target)
    return target, ok
}

func main() {
    err := fmt.Errorf("wrap: %w", new("I am a myError"))

    fmt.Println(isMyError(err))
    fmt.Println(asMyError(err))

    err = fmt.Errorf("wrap: %w", errors.New("I am not a myError"))

    fmt.Println(isMyError(err))
    fmt.Println(asMyError(err))
}

我预料到了

true
I am a myError true
false
I am not a myError false

但是我得到了

false
I am a myError true
false
%!v(PANIC=Error method: runtime error: invalid memory address or nil pointer dereference) false

我尝试添加

func (e myError) Unwrap() error        { return e.err }
func (e myError) As(target any) bool   { return errors.As(e.err, target) }
func (e myError) Is(target error) bool { return errors.Is(e.err, target) }

我试过了

func asMyError(err error) (error, bool) {
    target := &myError{} // was 'var target myError' before
    ok := errors.As(err, &target)
    return target, ok
}

我试过了

func new(msg string, args ...any) error {
    return &myError{fmt.Errorf(msg, args...)} // The change is the character '&'
}

但这些都没有改变任何事情。我也试过了

func asMyError(err error) (error, bool) {
    target := new("") // // was 'var target myError' or 'target := &myError{}' before
    ok := errors.As(err, &target)
    return target, ok
}

然后我得到了

false
wrap: I am a myError true
false
wrap: I am not a myError true

,我认为这是有道理的,但又不能解决我的问题。我很难解决这个问题。你能帮我一下吗?

go error-handling equality
2个回答
3
投票

因此

errors.Is
errors.As
的要点是可以包装错误,并且这些函数允许您提取给定错误的根本原因。这本质上依赖于具有特定错误值的某些错误。以此为例:

const (
    ConnectionFailed   = "connection error"
    ConnectionTimedOut = "connection timed out"
)

type PkgErr struct {
    Msg string
}

func (p PkgErr) Error() string {
    return p.Msg
}

func getError(msg string) error {
    return PkgErr{
        Msg: msg,
    }
}

func DoStuff() (bool, error) {
    // do some stuff
    err := getError(ConnectionFailed)
    return false, fmt.Errorf("unable to do stuff: %w", err)
}

然后,在调用者中,您可以执行以下操作:

_, err := pkg.DoStuff()
var pErr pkg.PkgErr
if errors.As(err, &pErr) {
    fmt.Printf("DoStuff failed with error %s, underlying error is: %s\n", err, pErr)
}

或者,如果您只想处理连接超时,但连接错误应该立即失败,您可以执行以下操作:

accept := pkg.PkgErr{
    Msg: pkg.ConnectionTimedOut,
}
if err := pkg.DoStuff(); err != nil {
    if !errors.Is(err, accept) {
        panic("Fatal: " + err.Error())
    }
    // handle timeout
}

本质上,您不需要为 unwrap/is/as 部分实现任何内容。这个想法是,你得到一个“通用”错误返回,并且你想要解开你知道的底层错误值,并且你可以/想要处理。如果有的话,在这一点上,自定义错误类型与其说是一个附加值,不如说是一个麻烦。使用这种包装/errors.Is的常见方法是将错误作为变量:

var (
    ErrConnectionFailed   = errors.New("connection error")
    ErrConnectionTimedOut = errors.New("connection timed out")
)
// then return something like this:
return fmt.Errorf("failed to do X: %w", ErrConnectionFailed)

然后在调用者中,您可以通过执行以下操作来确定出现问题的原因:

if errors.Is(err, pkg.ErrConnectionFailed) { panic("connection is borked") } else if errors.Is(err, pkg.ErrConnectionTimedOut) { // handle connection time-out, perhaps retry... }

可以在 SQL 包中找到如何使用它的示例。驱动程序包有一个像
driver.ErrBadCon

这样定义的错误变量,但是数据库连接的错误可能来自各个地方,因此当与这样的资源交互时,您可以通过执行以下操作快速找出问题所在:

 if err := foo.DoStuff(); err != nil {
    if errors.Is(err, driver.ErrBadCon) {
        panic("bad connection")
    }
 }

我自己并没有真正使用过
errors.As

那么多。 IMO,返回错误并根据错误的确切内容将其进一步传递到要处理的调用堆栈感觉有点错误,甚至:提取底层错误(通常删除数据),将其传递回去。我想它可以用于错误消息可能包含您不想发送回客户端或其他内容的敏感信息的情况:

// dealing with credentials:
var ErrInvalidData = errors.New("data invalid")

type SanitizedErr struct {
    e error
}

func (s SanitizedErr) Error() string { return s.e.Error() }

func Authenticate(user, pass string) error {
    // do stuff
    if !valid {
        return fmt.Errorf("user %s, pass %s invalid: %w", user, pass, SanitizedErr{
            e: ErrInvalidData,
        })
    }
}

现在,如果此函数返回错误,为了防止用户/密码数据以任何方式记录或发送回,您可以通过执行以下操作提取通用错误消息:

var sanitized pkg.SanitizedErr _ = errors.As(err, &sanitized) // return error return sanitized

总而言之,这已经成为该语言的一部分已有相当长的一段时间了,而且我还没有看到它被广泛使用。不过,如果您希望自定义错误类型实现某种 
Unwrap

功能,那么执行此操作的方法确实非常简单。以这个经过清理的错误类型为例:

func (s SanitizedErr) Unwrap() error {
    return s.e
}

仅此而已。要记住的是,乍一看, 
Is

As
函数以递归方式工作,因此您使用的实现此
Unwrap
函数的自定义类型越多,实际展开所需的时间就越长。这甚至没有考虑到您最终可能会遇到这样的情况:
boom := SanitizedErr{}
boom.e = boom

现在 
Unwrap

方法只会一遍又一遍地返回相同的错误,这只会导致灾难。在我看来,你从中获得的价值是相当小的。

    


0
投票
Elias 的回答

这个 go.dev 博客
之后,我想我更好地理解了 Go 和 errors 包中的错误。 实际上,Go 中的错误似乎有两种主要范例:

    包定义/导出常量“哨兵”错误,这些错误是从该包的函数返回的。这适用于不需要附加任何附加数据的错误。
  1. const ( ErrTimeout = errors.New("timeout") ErrNotFound = errors.New("not found") ... )

    从此包返回的错误可以通过值
    与哨兵错误进行比较。过去,这种比较只是用

    == 来进行,但在错误包装时代,你应该使用 errors.Is

     来进行比较。
    res, err := pkg.DoThing()
    if errors.Is(err, pkg.ErrTimeout) {
      ...
    }
    

    包定义/导出自定义错误
  2. types
  3. ,这些错误是从该包的函数返回的。这适合将附加数据/上下文/信息附加到错误。

    type IOError struct { filename string exitCode int action string errorInfo string } func (e *IOError) Error() string { return fmt.Sprintf("%s file %q: %s", e.action, e.filename, e.errorInfo) }

    从此包返回的错误应将其
    types
    与导出的错误类型进行比较。在过去,这可以通过对错误进行类型断言来完成,但在错误包装时代,您应该使用

    errors.As 来进行比较。

    res, err := pkg.DoThing()
    ioError := &pkg.IOError{}
    if errors.As(err, &ioError) {
      os.Remove(ioError.filename)
      ...
    }
    

        
© www.soinside.com 2019 - 2024. All rights reserved.