将外部错误映射到 golang 中的域错误

Mapping external errors to domain errors in golang

我有一个名为 ComputeService 的服务类型,它实现了特定的域逻辑。服务本身依赖于名为 Computer 的接口的实现,该接口具有方法 Computer.Compute(args...) (value, error)。如图所示,Compute 本身可能 return 某些错误。

ComputeService 需要使用正确的域错误代码从一组域错误中发送适当的错误,以便可以完成翻译,并且客户端也可以适当地处理错误。

我的问题是,Computer 实现应该将它们的失败包装在域错误中,还是应该 ComputeService 这样做。如果 ComputeService 是执行此操作的人,那么它将必须了解 return 由 Computer 接口的不同实现引起的不同错误,在我看来,这打破了抽象。两种方式演示如下:

package arithmetic
type Computer struct {
}
func (ac Computer) Compute(args ....) (value, error) {
     // errors is a domain-errors package defined in compute service project
     return errors.NewDivideByZero()
}

package compute
type Service struct {
}
func (svc Service) Process(args...) error {
    computer := findComputerImplementation(args...)
    val, err := computer.Compute(args...)
    if err != nil {
       if err == arith.ErrDivideByZero {
          // converting an arithmetic computer implementation 
          // specific error to domain error
          return errors.NewDivideByZero()
       } else if err == algebra.ErrInvalidCoEfficient {
          // converting an algebraic computer implementation 
          // specific error to domain error
          return errors.NewBadInput()
       }
       // some new implementation was used and we have no idea
       // what errors it could be returning. so we have to send
       // a internal server error equivalent here
       return errors.NewInternalError()
    }

}

Computer 的实施者应该用域错误来响应,因为它们是最接近操作的错误并且最能确定错误是什么。就像你说的那样,inComputeService 的逻辑打破了抽象。如果您需要将代码从特定 Computer 错误映射到域错误,请创建将主要逻辑与该错误包装代码分开的包装器结构。

要保留内部错误上下文,只需将原始错误嵌入到域错误中并创建 IsSpecificDomainError 个助手。

type MyDomainError struct {
    Err error
}

func NewMyDomainErr(err error) error {
    return &MyDomainError{err}
}

func IsMyDomainError(e error) bool {
    _, ok := err.(*MyDomainError)
    return ok
}

To keep internal error context, just embed the original error in the domain error

这可以使用来自 issue 29934, as detailed here.

的 Go 1.13(2019 年第 4 季度)的包装错误

err.Is():

拉斯·考克斯 mentions:

I think we all agree that strings.Contains(err.Error(), "not found") is fragile code.

I hope we also agree that we'd prefer to see code like errors.Is(err, os.ErrNotExist).

But the point is that in many cases, it is essential to future evolution of a package to keep callers from depending on a particular error result satisfying errors.Is(err, os.ErrNotExist), even if that is the underlying cause in today's result.
It's like looking at an unexported field or comparing error text - it's a detail that might change.

And while strings.Contains looks and is fragile, errors.Is does not look nor should be considered fragile.
If we are to avoid it being fragile, then we need to provide a way for packages to report detail without letting clients test for it. That way is errors that can't be unwrapped.

err.As():

var pe *os.PathError
if errors.As(err, &pe) {
     use(pe)
}

%w:

func inner() error { return errors.New("inner error") }
func outer() error { return fmt.Errorf("outer error: %w", inner()) }

fmt.Fprintf("%+v", outer())
// outer error:
//     /path/to/file.go:123
//   - inner error:
//     /path/to/file.go:122

current status for Go 1.13:

Just stating what I see as the compromise solution offered by the team:

  • fmt.Errorf is currently being used extensively to wrap errors and return a new (opaque) error (as in you cannot access the underlying error).
    '%w' can now be used to explicitly opt-in to return an error that can be unwrapped.
  • errors is designed as a base package with no dependencies so that every package can depend on it.
  • the team agrees to punt on the areas that there is broad disagreement, and want to release just enough (errors.Is, errors.As, extension to way most folks wrap errors) so folks can achieve things.
  • Generics is not here yet, and we do not know when it will come: the heated discussion on that will make this one on "error 2 values" look like child's play.
    errors.Is and errors.As are clean and concise enough to be comfortable for a long time.

Most of the contentious things have been punted to go 1.14.

  • Wrapf cannot live in errors as it is a base package.
  • Wrapf means team MUST decide on what happens when a nil error is passed: Punt on it.
  • Wrap may conflict with ideas being considered for localization, internationalization, etc.
  • ErrorFormatter and ErrorPrinter have not yet gotten much deeper usage and have warts. Punt.