【问题标题】:Should I use panic or return error?我应该使用恐慌还是返回错误?
【发布时间】:2017-06-12 16:27:56
【问题描述】:

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

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

【问题讨论】:

标签: go error-handling panic


【解决方案1】:

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

【讨论】:

  • 同意。但是我如何确定程序是否可以继续?在我看来,如果不小心传入了某个整数,程序就没有办法继续运行了。
  • 可能是这样。也许“可以继续”有点模糊 - 它的真正含义是,如果发生错误,您的程序有什么方法可以处理错误吗?一般来说,在 go 中,如果不确定,返回错误。恐慌应该很少见。
  • 这个回复不是一个答案:它是一个问题(问问你自己...)。
  • 完整阅读。它鼓励读者问自己一个问题,以便根据答案提供逻辑来为自己做出决定。
  • 同时,来自官方博客的post 提出了一种不同的方法。他们以json library 为例,它在内部使用panic 并将其转换为公共API 中的错误。
【解决方案2】:

使用panic

因为您的用例是发现对 API 的不当使用。如果程序正确调用您的 API,则在运行时绝不应该发生这种情况。

事实上,任何使用正确参数调用您的 API 的程序都会以相同的方式运行如果测试被删除。测试只是在早期失败,错误消息有助于犯错的程序员。理想情况下,在开发过程中运行测试套件时可能会发生一次恐慌,并且程序员甚至会在提交错误代码之前修复调用,并且错误的使用永远不会进入生产环境。

另请参阅this reponse 以提问在 Go 中使用错误进行函数参数验证是一种好的模式吗?

【讨论】:

【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      问自己这些问题:

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

      【讨论】:

        【解决方案6】:

        尽可能使用错误

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

        但错误不像 try/catch

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

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

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

            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呢?

        当您的代码在碰撞过程中崩溃并且您无法假设自己的出路时。另一个是当代码假设某些不再正确的东西并且必须从这里开始检查每个函数的完整性将是乏味的(并且可能会影响性能)。尽管如此,您仍会使用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

        我们认为,将异常耦合到控制结构(如 try-catch-finally 习惯用法)会导致代码复杂化。它还倾向于鼓励程序员将过多的普通错误(例如无法打开文件)标记为异常。

        Go 采用了不同的方法。对于简单的错误处理,Go 的多值返回可以很容易地报告错误而不会重载返回值。规范的错误类型与 Go 的其他特性相结合,使错误处理变得愉快,但与其他语言中的处理完全不同。

        【讨论】:

          【解决方案7】:

          恐慌通常意味着出现意外错误。通常用于在正常操作期间不应该发生的错误或我们不准备优雅处理的错误上快速失败。所以在这种情况下只返回错误,你不希望你的程序恐慌。

          【讨论】:

          • 定义“正常操作”。因为这就是问题的重点:“检查isNotIterable 是否正常运行?”
          【解决方案8】:

          我认为之前的答案都不是正确的:

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

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

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

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

          【讨论】:

          • 我认为您的回答存在严重缺陷和有害建议。 Go 允许通过将错误作为其自身的值返回来快速失败,因此您可以随时返回失败消息。该错误还允许您通过检查收到的错误类型来重置为稳定状态;甚至比恐慌更容易。当你不能自己恢复控制流时,你会使用恐慌,因为如果没有恢复,恐慌会使程序崩溃。如果您在没有其他 goroutine 操作并且知道为什么会出现错误的情况下立即恢复调用堆栈,那么您做错了。
          • @dasper:如果您可以立即恢复通话,则没有错误。您有一组标准的可能值,其中一些代表一些“心理上的负面”非快乐值。但是 Touring 机器可以只返回一个定义的值、超时或失败并出现未定义的行为,而永远不会出现错误。如果考虑到错误,则它只是定义良好的一组返回值的一部分。
          • 不同意 Go 成语是合理的,但并不能改变它们是 Go 成语的事实,而且 StackOverflow 不是讨论它们的地方。在 Go 中,accepted practice is to expose errors。 “错误的返回值大多是一个错误的想法,即使 GO 社区已将其视为一种宗教”,在整个 Go 代码语料库中飞来飞去,并将设立任何跟随它的人作为 Go 开发人员的失败除非他们专门从事个人项目。
          【解决方案9】:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2022-01-22
            • 2018-10-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-08-31
            • 1970-01-01
            • 2015-03-29
            相关资源
            最近更新 更多