我应该使用恐慌还是 return 错误?

Should I use panic or return error?

Go 提供了两种处理错误的方法,但我不确定使用哪一种。

假设我正在实现一个经典的 ForEach 函数,它接受切片或映射作为参数。要检查是否传入了可迭代对象,我可以这样做:

func ForEach(iterable interface{}, f interface{}) {
    if isNotIterable(iterable) {
        panic("Should pass in a slice or map!")
    }
}

func ForEach(iterable interface{}, f interface{}) error {
    if isNotIterable(iterable) {
        return fmt.Errorf("Should pass in a slice or map!")
    }
}

我看到一些讨论说 panic() 应该避免,但人们也说如果程序无法从错误中恢复,你应该 panic().

我应该使用哪一个?选择合适的主要原则是什么?

您应该假设对于整个程序或至少对于当前的 goroutine 来说,恐慌将立即致命。问问自己“发生这种情况时,应用程序是否应该立即崩溃?”如果是,请使用 panic;否则,使用错误。

恐慌通常意味着出现意外错误。主要用于在正常操作期间不应该发生的错误上快速失败,或者我们不准备妥善处理的错误。所以在这种情况下只是 return 错误,您不希望您的程序出现 panic。

不要将 panic 用于正常的错误处理。使用错误和多个 return 值。参见 https://golang.org/doc/effective_go.html#errors

如果在启动服务时未提供或不存在某些强制性要求(例如,数据库连接、某些必需的服务配置),那么您应该使用 panic。

任何用户响应或服务器端错误都应该 return 错误。

使用panic.

因为您的用例是 发现您的 API 的不当使用。如果程序正确调用您的 API,这在运行时永远不会发生。

事实上,任何使用正确参数调用您的 API 的程序都将以相同的方式运行如果测试被删除。那里的测试只会提前失败,并显示一条对犯错的程序员有帮助的错误消息。理想情况下,当 运行 测试套件和程序员甚至在提交错误代码之前修复调用时,恐慌可能会在开发过程中出现一次,并且这种不正确的使用永远不会影响生产。

另见 this reponse 来质疑 在 Go 中使用错误的函数参数验证是一个好的模式吗?

问自己这些问题:

  • 无论您对应用程序的编码有多好,您是否预计会出现异常情况?您认为在正常使用您的应用程序时让用户了解这种情况是否有用?将其作为错误处理,因为它与正常工作的应用程序有关。
  • 如果您适当地编码(并且有点防御性),这种异常情况应该不会发生吗? (例如:除以零,或越界访问数组元素)您的应用程序是否在该错误下完全无能为力?恐慌。
  • 您有 API 并希望确保用户正确使用它吗?恐慌。如果使用不当,您的 API 将很难恢复。

我喜欢它在一些库中完成的方式,在这些库中,在常规方法 DoSomething 之上,其“恐慌”版本添加了 MustDoSomething。我对 go 比较陌生,但我已经在几个地方看到过它,特别是 sqlx.
一般来说,如果您想将代码公开给其他人,您应该拥有 Must- 和该方法的常规版本,或者您的 methods/functions 应该让客户有机会恢复他们想要的方式因此 error 应该以 go 惯用的方式提供给他们。
话虽如此,我同意如果您的 API/library 使用不当,也可以恐慌。事实上,我也见过像 MustGetenv() 这样的方法,如果缺少关键的 env.var 就会出现恐慌。 Fail-fast基本机制。

我认为前面回答中none条是正确的:

更正式地说,我们的“图灵机”坏了,我们需要回到“稳定状态”或“重置状态”。更多信息请见 https://en.wikipedia.org/wiki/Reset_(computing)

例如在网络(微)服务中,这意味着 return 出现 40X 错误(由用户输入引起的恐慌)或 50X 错误(由其他原因引起的恐慌 - 硬件,网络,断言错误,. ..)

  • 如果我们知道如何处理“错误”,那么我们首先没有错误,而是一个不舒服的 return 值。这是正常的执行条件,可能不是错误。通常这对应于快乐与非快乐路径建模。

总而言之,错误的 return 值在很大程度上是一个错误的想法,即使 GO 社区已经将其作为一种宗教信仰。使用错误 return 值只是一种加速程序执行的补丁方法,因为它需要较少的 CPU 指令来实现,但大多数时候,除了低级服务之外,它是无用的并且促进脏代码。 (请注意,GO 旨在将这些低级服务实现为“easy-C”,但当错误必须快速失败以避免继续使用可能潜在的未定义状态时,它被用于高级(第 7 级)应用程序造成金钱损失或致命伤亡。如有疑问,默认为恐慌。

尽可能使用错误

仅当您的代码可能最终处于很容易崩溃的不良状态时才使用 panic;真正出乎意料的事情。上面带有 ForEach() 的示例是一个接受接口的导出函数,因此它应该预料到有人会不正确地调用它。如果调用不当,您就会知道无法继续的原因,并且知道如何处理该错误。 isNotIterable字面上是二进制的,易于控制。

但错误不像是 try/catch

即使您试图通过查看其他语言的 throw/catch 来证明 panic/recover 的合理性,您仍然会使用错误。我们知道你正在尝试这个函数,因为你正在调用它,我们知道有一个错误,因为 err != nil,就像检查抛出的异常类型一样,你可以检查 errors.Is(err, ErrNotIterable)[ 返回的错误类型=21=]

那么你应该对并发错误使用 panic 吗?

答案很可能仍然是否定的。错误仍然是 Go 中的首选方式,您可以使用等待组来关闭 goroutines:

    ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
    // automatically cancel in 5 min
    defer cancel()
    errGroup, ctx := errgroup.WithContext(ctx)
    errGroup.Go(func() error {
        // do crazy stuff; you can still check for errors
        if ... {
            return fmt.Errorf("critical error, stopping all goroutines now")
        }
        // code completed without issues
        return nil
    })
    err = errGroup.Wait()

即使使用原始示例的结构,与恐慌相比,您仍然可以更好地控制错误:

func ForEach(iterable interface{}, f interface{}) error {
    if isNotIterable(iterable) {
        return fmt.Errorf("expected something iterable but got %v", reflect.ValueOf(iterable).String())
    } 
    
    switch v.Kind() {
    case reflect.Map:
        ...
    case reflect.Array, reflect.Slice: 
        ...
    default:
        return fmt.Errorf("isNotIterable is false but I do not know how to iterate through %v", reflect.ValueOf(iterable).String())
}

但是错误感觉很冗长

是的,这就是重点。当返回一个错误时,就是在那个时候做一些事情。您正在为调用代码提供选项,而不是决定开始关闭并终止应用程序 除非recover()。如果您只是在调用堆栈中一直返回相同的错误,那么错误似乎不如恐慌,但这是由于在问题发生时没有解决问题。

那么什么时候使用恐慌?

当您的代码处于崩溃的碰撞过程中并且您无法确定自己的出路时。另一种情况是,当代码假设某些东西不再为真时,从现在开始必须检查每个函数的完整性将是乏味的(并且可能会影响性能)。尽管如此,您还是会使用 panic() 来摆脱不确定性层...然后仍然处理错误:

func ForEach(iterable interface{}, f interface{}) error {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("cannot iterate due to unexpected runtime error %v", r)
            return
        }
    }()
    ...
    // perhaps a broken pipe in a global var
    // or an included module threw a panic at you!
}

但是如果你还是不服... Here is the Go FAQ

We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.

Go takes a different approach. For plain error handling, Go's multi-value returns make it easy to report an error without overloading the return value. A canonical error type, coupled with Go's other features, makes error handling pleasant but quite different from that in other languages.