自从我上次发布有关更好的Golang错误处理以来已经有一段时间了,我想我会跟进这是如何以真实代码结束的。
最终错误设计
现在的设计基本相同,但是有一些命名调整和额外的功能。我还熨了了很多有关错误传播的细节,并添加了一些更有趣的概念。
type Cause interface {
~uint
}
type Error[T Cause] struct {
Cause T
rootCause any
Message string
// Add as much extra detail as you wish!
}
func NewError[T Cause](ctx context.Context, cause T, message string, values ...any) Error[T] {
// ...
}
func FromError[T Cause, R Cause](cause T, origin Error[R]) Error[T] {
// ...
}
func Ok[T Cause]() Error[T] {
return Error[T]{}
}
// Error message.
//
// Satisfies the error interface.
func (self Error[T]) Error() string {
return self.String()
}
func (self Error[T]) IsOk() bool {
return self.Cause == 0
}
func (self Error[T]) IsErr() bool {
return self.Cause != 0
}
func (self Error[T]) String() string {
// ...
}
请注意,Error
现在仅仅是重要作品Cause
的传输机构。处理Error
时,我们检查其Cause
,以查看导致该功能失败的原因。
传播错误时,现在有FromError()
选项。这使您可以简单地从函数中删除错误,然后将其作为函数的另一个错误返回。请注意,这只是在周围复制数据,场景后面没有链接列表。我确实发现保留rootCause
以及当前的Cause
非常有用。这样我们就可以记录第一个Cause
,同时仍然能够对不同的Cause
做出反应,因为我们将错误传递到呼叫堆栈。
Error
比Golang的错误最大的好处之一是它需要零堆的分配。如上所述,Error
在各个方面都显然是优于golang的错误。它确实可以完成Golang可以做的事情,但是在CPU时间和内存分配的一小部分中。我鼓励您自己对其进行基准测试。
请注意,您可以将任何想要的内容添加到Error
。它只是为您的根本原因作为元数据。如果您希望使错误更重,但更具描述性,则可以添加代码,堆栈跟踪,指标,记录等等。在这个简单的结构中,您可以发挥很多力量。这只是您项目的起点。
错误用法
这是使用它的示例。
type MyError uint
const (
MyErrorInternalFailure = MyError(iota + 1)
MyErrorNotFound
)
func getThing(ctx context.Context) Error[MyError] {
if true {
return NewError(ctx, MyErrorNotFound, "could not find the thing")
}
return errors.Ok[MyError]()
}
func main() {
ctx := context.Background()
switch getThingErr := getThing(ctx); getThingErr.Cause {
case MyErrorInternalFailure:
fmt.Println("failed finding the thing, internal failure: " + getThingErr.Error())
case MyErrorNotFound:
fmt.Println("failed finding the thing, not found: " + getThingErr.Error())
default:
// Found the thing!
}
}
考虑到哪些错误会导致创建什么,请考虑要测试的重要内容。每个故障情况都应在功能上具有自己的Cause
枚举价值。例如,一个函数可以返回NotFound
,InternalFailure
,BadRequest
等。这意味着您可以轻松地测试不同的情况。我发现牢记测试有助于指导您的编码决策。
当消费者处理NotFound
案例时,它可能不在乎,完全忽略它,或者可能将其与函数返回的另一个错误相结合。在说您的存储层具有一堆特定于存储的错误情况的情况下,重组错误原因可能会有用,例如MySqlTransactionFailure,但是调用函数只是将它们全部插入通用的InternalFailure
,原因是它返回。
此模式的另一个重要点是,您只有两个返回值:(<value>, <error>)
。任何“软”错误可能不存在您的价值,都应包含您的错误导致枚举。这消除了您没有价值的尴尬案例,但也没有错误,并且您会发现(在Golang错误中)(nil, nil)
,您必须确定这实际上是什么意思。保持此操作或不可用的方法可确保您的错误switch
始终处理每个可能的情况 可能的情况,并且您的default
案例纯粹是为您的成功案例保留的,并且您可以保证一个价值。
开关模式的一个很好的副作用是,您通常可以使用开关范围保持变量,以避免错误地引用变量并保持代码清洁器。
知道您可以保证您有一个价值,并保证这些特定的故障原因在大型应用程序中变得强大。当您启用覆盖物(您应该已经拥有!)时,任何新的错误枚举值都将突出显示您需要处理此新情况的代码。删除案件时,您将获得编译错误,显示该案例不再有效。这使其使商业逻辑更改对大型现有应用程序变得更加容易,因为您的代码突出了您需要更改的所有内容。它还迫使您考虑新案件。假设您在应用程序内部的功能中添加了UserNotFound
盒。如果您使用的是Golang错误,则可以添加此案例并认为您已经处理了它,因为所有内容都是error
并与if err != nil
进行了检查。但是,由于每个功能都键入其自身可能的错误原因,因此您现在被迫在整个应用程序中处理此情况。这有助于消除虫子在存在之前!我可以证明,我对这种错误模式使我免于犯逻辑错误感到非常惊喜。
我鼓励您使用此示例代码并在您的应用程序中为您自己的需求进行扩展。在Go Playground上查看完整示例!