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

huangapple go评论79阅读模式
英文:

Applying `errors.Is` and `errors.As` on custom made struct errors

问题

以下是你提供的代码的翻译:

package main

import (
	"errors"
	"fmt"
)

type myError struct{ err error }

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

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

func (e myError) Unwrap() error        { return e.err }
func (e myError) As(target interface{}) 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 interface{}) 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{} // 之前是 'var target myError'
    ok := errors.As(err, target)
    return target, ok
}

我尝试了以下代码:

func new(msg string, args ...interface{}) error {
	return &myError{fmt.Errorf(msg, args...)} // 改动是字符 '&'
}

但是这些改动都没有产生任何效果。我还尝试了以下代码:

func asMyError(err error) (error, bool) {
	target := new("") // 之前是 'var target myError' 或 'target := &myError{}'
	ok := errors.As(err, &target)
	return target, ok
}

然后我得到了以下输出:

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

我猜这是有道理的,但仍然没有解决我的问题。我很难理解这个问题。你能帮我一下吗?

英文:
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))
}

I expected

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

but I got

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

I tried to add

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) }

I tried

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

I tried

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

but none of these changed anything. I also tried

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

and then I got

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

, which I guess makes sense but again does not solve my problem. I have a hard time to wrap my head this problem. Can you give me a hand?

答案1

得分: 3

errors.Iserrors.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) {
    // 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())
    }
    // 处理超时
}

在解包/判断错误类型方面,你实际上不需要实现任何内容。思路是你得到一个“通用”的错误返回,然后你想要解包你知道并且可以处理的根本错误值。事实上,此时自定义错误类型更像是一个麻烦而不是增加的价值。使用这种包装/errors.Is的常见方式是将错误作为变量:

var (
    ErrConnectionFailed   = errors.New("connection error")
    ErrConnectionTimedOut = errors.New("connection timed out")
)
// 然后返回类似这样的内容:
return fmt.Errorf("failed to do X: %w", ErrConnectionFailed)

然后在调用者中,你可以通过以下方式确定出错的原因:

if error.Is(err, pkg.ErrConnectionFailed) {
    panic("connection is borked")
} else if error.Is(err, pkg.ErrConnectionTimedOut) {
    // 处理连接超时,可能重试...
}

这在 SQL 包中的使用示例中可以找到。驱动程序包定义了一个名为 driver.ErrBadCon 的错误变量,但是来自数据库连接的错误可能来自各个地方,因此在与此类资源交互时,你可以通过以下方式快速确定出错原因:

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

我自己并没有真正经常使用 errors.As。在我看来,返回一个错误,并将其传递到调用堆栈上方以根据错误的确切情况进行处理,甚至提取底层错误(通常会删除数据),然后将其传递回去,感觉有点不对。我想它可以用于错误消息可能包含敏感信息的情况,你不希望将其发送回客户端之类的情况:

// 处理凭据:
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 {
    // 做一些事情
    if !valid {
        return fmt.Errorf("user %s, pass %s invalid: %w", user, pass, SanitizedErr{
            e: ErrInvalidData,
        })
    }
}

现在如果此函数返回错误为了防止用户/密码数据被记录或以任何方式发送回去你可以通过以下方式提取通用错误消息

```go
var sanitized pkg.SanitizedErr
_ = errors.As(err, &sanitized)
// 返回错误
return sanitized

总的来说,这已经是语言的一部分很长时间了,我并没有看到它被广泛使用。如果你希望自定义错误类型实现类似 Unwrap 函数,那么实现起来确实非常简单。以这个经过清理的错误类型为例:

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

就是这样。需要记住的是,乍一看,IsAs 函数是递归工作的,所以你使用实现了 Unwrap 函数的自定义类型越多,实际的解包时间就越长。这甚至还没有考虑可能出现这种情况的情况:

boom := SanitizedErr{}
boom.e = boom

现在 Unwrap 方法将简单地一次又一次地返回相同的错误,这只会导致灾难。总的来说,我认为你从中获得的价值是相当有限的。

英文:

So the point of errors.Is and errors.As is that errors can be wrapped, and these functions allow you to extract what the underlying cause of a given error was. This essentially relies on certain errors having specific error values. Take this as an example:

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) {
    // stuff
    err := getError(ConnectionFailed)
    return false, fmt.Errorf("unable to do stuff: %w", err)
}

Then, in the caller, you can do something like this:

_, 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)
}

Or, if you only want to handle connection timeouts, but connection errors should instantly fail, you could do something like this:

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

There is, essentially, nothing you need to implement for the unwrap/is/as part. The idea is that you get a "generic" error back, and you want to unwrap the underlying error values that you know about, and you can/want to handle. If anything, at this point, the custom error type is more of a nuisance than an added value. The common way of using this wrapping/errors.Is thing is by just having your errors as variables:

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)

Then in the caller, you can determine why something went wrong by doing:

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

An example of how this is used can be found in the SQL packages. The driver package has an error variable defined like driver.ErrBadCon, but errors from DB connections can come from various places, so when interacting with a resource like this, you can quickly work out what went wrong by doing something like:

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

I myself haven't really used the errors.As all that much. IMO, it feels a bit wrong to return an error, and pass it further up the call stack to be handled depending on what the error exactly is, or even: extract an underlying error (often removing data), to pass it back up. I suppose it could be used in cases where error messages could contain sensitive information you don't want to send back to a client or something:

// 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,
        })
    }
}

Now, if this function returns an error, to prevent the user/pass data to be logged or sent back in any way shape or form, you can extract the generic error message by doing this:

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

All in all though, this has been a part of the language for quite some time, and I've not seen it used all that much. If you want your custom error types to implement an Unwrap function of sorts, though, the way to do this is really quite easy. Taking this sanitized error type as an example:

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

That's all. The thing to keep in mind is that, at first glance, the Is and As functions work recursively, so the more custom types that you use that implement this Unwrap function, the longer the actual unwrapping will take. That's not even accounting for situations where you might end up with something like this:

boom := SanitizedErr{}
boom.e = boom

Now the Unwrap method will simply return the same error over and over again, which is just a recipe for disaster. The value you get from this is, IMO, quite minimal anyway.

huangapple
  • 本文由 发表于 2022年5月19日 23:40:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/72307412.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定