How to create a custom error that's a subtype of another custom error, like inherited?

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

How to create a custom error that's a subtype of another custom error, like inherited?

问题

给定一个自定义错误类型:

type CustomError struct {
    // Err optionally wraps the original error.
    Err error `json:"-"`

    // Human readable message.
    Message string `json:"message" validate:"required,gte=3"`

    // StatusCode is a valid HTTP status code, e.g.: 404.
    StatusCode int `json:"-"`
}

该类型实现了Error() stringUnwrap() error接口,并且还有一个工厂函数:

func NewCustomError(m string, s int, e error) *CustomError {}

如何基于CustomError创建另一个"类型",我们称之为FailedTo,用于表示类似"Failed to created X"的错误,默认情况下具有以下特点:

  • Message以"Failed to"为前缀
  • 状态码为500

除此之外,还有另一个类型,比如FailedToCreateSomething,它的创建方式如下:

func createSomething() error {
    return FailedToCreateSomething(errors.New("File is busy"))
}

errCreateSomething := createSomething()

...errCreateSomething的类型是FailedToCreateSomething,同时也是FailedToCustomError类型的实例。

英文:

For a given custom error type:

type CustomError struct {
	// Err optionally wraps the original error.
	Err error `json:"-"`

	// Human readable message.
	Message string `json:"message" validate:"required,gte=3"`

	// StatusCode is a valid HTTP status code, e.g.: 404.
	StatusCode int `json:"-"`
}

Which implements both Error() string, and the Unwrap() error interface, also have a factory:

func NewCustomError(m string, s int, e error) *CustomError {}

How to create another "type", based on CustomType - let's call it FailedTo for errors such as "Failed to created X" which will have by default:

  • A prefixed Message with Failed to
  • Status code of 500

and on top of that, another one, such as FailedToCreateSomething in a way where....

func createSomething() error {
    return FailedToCreateSomething(errors.New("File is busy"))
}

errCreateSomething := createSomething()

...errCreateSomething is type of FailedToCreateSomething, also FailedTo, and CustomError?

答案1

得分: 3

让我们将示例简化到其本质。

package customerror

import (
	"errors"
)

type CustomError struct {
	Aux string
	Err error
}

func (cE *CustomError) Error() string { /*...*/ }
func (err *CustomError) Unwrap() error { return err.Err }

func NewFailedToError(aux string, err error) error {
    return &CustomError{ Aux: aux, Err: err }
}

var FailedToWriteToFile = NewFailedToError("write to file", nil)

现在我们可以回答代码片段中的问题:

// 仅用于演示的某个函数。
func WriteToFile() error {
    // 某些原因导致错误..
    errSome := errors.New("Failed to open file")

    // 如何将`FailedToWriteToFile`的`err`值设置为`errSome`,
    // 而不是为所有`FailedToWriteToFile`实例(指针)设置该值?
    // 同时仍然使其为`FailedToWriteToFile`类型(errors.Is(errSome, FailedToWriteToFil))?
    return FailedToWriteToFile
}

让我们将这个问题重新表述为如何创建一个具有新消息的errSome,其中errors.Is(errSome, FailedToWriteToFil)成立。

我们可以查看errors.Is的文档:
> Is报告err链中的任何错误是否与目标匹配。
>
> 该链由err本身和通过重复调用Unwrap获得的错误序列组成。
>
> 如果一个错误等于目标或者实现了一个方法Is(error) bool,使得Is(target)返回true,则认为该错误与目标匹配。
>
> 错误类型可能会提供一个Is方法,以便将其视为等同于现有错误。例如,如果MyError定义了
>
> func (m MyError) Is(target error) bool { return target == fs.ErrExist }
>
> 那么Is(MyError{}, fs.ErrExist)将返回true。请参见标准库中的syscall.Errno.Is的示例。

这给我们提供了两种方法。一种方法是将FailedToWriteToFile放在Unwrap链上。如果我们使用Err字段指向FailedToWriteToFile,CustomError的字段足够实现这一点,例如:

&CustomError{Aux: "Failed to open file", Err: FailedToWriteToFile}

这个结构的Unwrap()等于FailedToWriteToFile。顺便说一句,如果你想捕获来自其他源的错误值,Aux可以是类型为error的字段。只要Unwrap()链最终导致FailedToWriteToFile即可。

另一种方法是在CustomError上定义一个Is谓词。有很多可能的谓词可供选择。一个简单的谓词是所有的*CustomError都被认为是等价的。可以这样实现:

func (e *CustomError) Is(target error) bool {
  _, ok := target.(*CustomError)
  return ok
}
英文:

Let's distill the example down to it's essence.

package customerror

import (
	"errors"
)

type CustomError struct {
	Aux string
	Err error
}

func (cE *CustomError) Error() string { /*...*/ }
func (err *CustomError) Unwrap() error { return err.Err }

func NewFailedToError(aux string, err error) error {
    return &CustomError{ Aux: aux, Err: err }
}

var FailedToWriteToFile = NewFailedToError("write to file", nil)

We can now get to the question in the gist:

// Some function just for demonstration.
func WriteToFile() error {
    // Something caused some error..
    errSome := errors.New("Failed to open file")

    // How can I set the `err` value of `FailedToWriteToFile` to `errSome`
    // without setting that for all `FailedToWriteToFile` instances (pointer)?
    // while still making it of `FailedToWriteToFile` type (errors.Is(errSome, FailedToWriteToFil))?
    return FailedToWriteToFile
}

Let's reframe this question to how do I make an errSome with a new message where errors.Is(errSome, FailedToWriteToFil) holds.

We can checkout the documentation of errors.Is:
> Is reports whether any error in err's chain matches target.
>
> The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.
>
> An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true.
>
> An error type might provide an Is method so it can be treated as equivalent to an existing error. For example, if MyError defines
>
> func (m MyError) Is(target error) bool { return target == fs.ErrExist }
>
> then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for an example in the standard library.

This gives us two paths. One of the paths is to put FailedToWriteToFile on the Unwrap chain. CustomError has enough fields for this if we use the Err field to point to FailedToWriteToFile, e.g.

&CustomError{Aux: "Failed to open file", Err: FailedToWriteToFile}

Unwrap() on this is == to FailedToWriteToFile. FWIW Aux could be field of type error if you are trying to capture an error value coming from another source. As long as the Unwrap() chain leads to the FailedToWriteToFile eventually.

The other path is to define an Is predicate on CustomError. There are a lot of possible predicates to choose from. A simple one is that all *CustomError are considered equivalent. That would be:

func (e *CustomError) Is(target error) bool {
  _, ok := target.(*CustomError)
  return ok
}

答案2

得分: 0

代码:

import (
	"fmt"
	logger "log"
	"os"
	"testing"
)

var log = logger.Default()

func TestErrorWrapping(t *testing.T) {
	_, err := os.ReadFile("wrong file path")
	if err != nil {
		// 使用源错误包装自定义错误
		err = fmt.Errorf("无法读取文件,%w", err)
		// 打印错误信息
		log.Printf("出现错误,%s", err.Error())
	}
}

输出:

出现错误,无法读取文件,打开错误的文件路径:系统找不到指定的文件。

英文:

Code:

import (
	"fmt"
	logger "log"
	"os"
	"testing"
)

var log = logger.Default()

func TestErrorWrapping(t *testing.T) {
	_, err := os.ReadFile("wrong file path")
	if err != nil {
        // custom error with wrapped a source error
		err = fmt.Errorf("can't read file, %w", err)
        // print it
		log.Printf("something went wrong, %s", err.Error())
	}
}

Output:
> something went wrong, can't read file, open wrong file path: The system cannot find the file specified.

huangapple
  • 本文由 发表于 2021年9月16日 13:46:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/69202926.html
匿名

发表评论

匿名网友

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

确定