Go语言 我应该使用panic还是return error?

4ioopgfo  于 2023-03-27  发布在  Go
关注(0)|答案(9)|浏览(235)

Go提供了两种处理错误的方法,但我不确定使用哪一种。
假设我正在实现一个经典的ForEach函数,它接受一个slice或一个map作为参数。要检查是否传入了一个iterable,我可以这样做:

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()
我应该使用哪一个?选择正确的方法的主要原则是什么?

qltillow

qltillow1#

你应该假设一个panic会立即对整个程序,或者至少对当前的goroutine是致命的,问问自己“当这个发生时,应用程序应该立即崩溃吗?”如果是,就使用panic;否则,使用错误。

whlutmcx

whlutmcx2#

使用panic

因为你的用例是捕获API的错误使用。如果程序正确调用API,那么这种情况永远不会在运行时发生。
事实上,任何使用正确参数调用API的程序都会以相同的方式运行 * 如果测试被删除 *。测试只是在早期失败,并向出错的程序员提供错误消息。理想情况下,在开发期间运行测试套件时可能会出现一次panic,程序员甚至会在提交错误代码之前修复调用。而不正确的使用永远不会达到生产。
另请参阅this reponse来提问 Is function parameter validation using errors a good pattern in Go?

avwztpqn

avwztpqn3#

我喜欢在一些库中这样做的方式,在常规方法DoSomething的顶部,它的“panicky”版本被添加了MustDoSomething。我对go相对较新,但我已经在几个地方看到过它,特别是sqlx
一般来说,如果你想将你的代码公开给其他人,你应该有Must-和一个常规版本的方法,或者你的方法/函数应该给予客户端一个机会来恢复他们想要的方式,所以error应该以go惯用的方式对他们可用。
话虽如此,我同意如果你的API/库使用不当,也可以恐慌。事实上,我也见过像MustGetenv()这样的方法,如果缺少关键的env.var,就会恐慌。基本上是快速故障机制。

ecr0jaav

ecr0jaav4#

如果在启动服务时没有提供一些强制性要求(例如,数据库连接,一些必需的服务配置),那么您应该使用panic。
任何用户响应或服务器端错误都应该返回错误。

ca1c2owp

ca1c2owp5#

问自己这些问题:

  • 无论您的应用程序编写得有多好,您是否希望发生异常情况?您认为让用户意识到这种情况作为应用程序正常使用的一部分是否有用?将其作为错误处理,因为它涉及应用程序正常工作。
  • 如果您的编码适当(并具有一定的防御性),这种异常情况就不会发生吗?(例如:被零除,或者访问数组元素越界)你的应用程序在这个错误下完全没有头绪吗?恐慌。
  • 你有自己的API,并希望确保用户正确使用它吗?恐慌。如果使用不当,你的API将很少恢复。
8ulbf1ek

8ulbf1ek6#

尽量使用error

只有在代码可能以一种很容易崩溃的糟糕状态结束时才使用panic;确实出乎意料。上面的ForEach()示例是一个导出的func,它接受一个接口,所以它应该预料到有人会不正确地调用它。如果它被不正确地调用,你知道为什么你不能继续,你知道如何处理这个错误。isNotIterable实际上是二进制的,很容易控制。

但error不像try/catch

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

那么并发出错应该使用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()

即使使用原始示例的结构,您仍然可以更好地控制错误而不是panic:

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

但是error感觉很冗长

是的,这就是问题所在。当错误被返回时,就应该在那个时候做些什么。你是在给调用代码提供选项,而不是决定开始关闭并杀死应用程序除非recover()。如果你只是在调用堆栈的所有位置返回相同的错误,那么error看起来就不如panic,但这是由于没有在问题发生时加以解决。

什么时候使用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!
}

但如果你还是不服气……这里是Go常见问题解答
我们认为,将异常耦合到控制结构中,就像try-catch-finally习惯用法那样,会导致代码复杂,也会鼓励程序员将太多的普通错误(如无法打开文件)标记为异常。
Go采用了不同的方法。对于普通的错误处理,Go的多值返回使得报告错误变得很容易,而不会重载返回值。规范的错误类型,加上Go的其他功能,使错误处理变得令人愉快,但与其他语言截然不同。

vddsk6oq

vddsk6oq7#

panic通常意味着发生了意外错误。通常用于在正常操作期间不应该发生的错误上快速失败,或者我们没有准备好优雅地处理错误。因此在这种情况下,只需返回错误,您不希望程序出现panic。

h9vpoimq

h9vpoimq8#

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

更正式地说,我们的“图灵机”坏了,我们需要回到“稳定状态”或“重置状态”。
例如,在web(微)服务中,这意味着返回40 X错误(由用户输入引起的死机)或50 X错误(由其他东西引起的死机-硬件,网络,Assert错误...)

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

总而言之,err返回值大多是一个错误的想法,即使GO社区已经将其作为一种宗教。使用错误返回值只是一种加速程序执行的修补方法,因为它需要更少的CPU指令来实现,但大多数时候,除了低级服务,它是无用且会促进脏代码。(注意,GO被设计为将那些低级服务实现为“easy-C”,但它被用于高级(7级)应用程序,当错误必须快速失败以避免继续未定义的状态时,这种状态可能会导致金钱损失或致命的伤亡。
更新2023-03:(添加Erlang参考)没有固执己见或惯用的方法来处理错误。有正确的方法和不正确的方法来处理错误。正确的方法是Erlang方法(设计为支持~100%的正常运行时间的语言)。C&P来自intro

  • 早期崩溃:
  • 让其他进程进行错误恢复
  • 不要进行防御性编程
  • 如果无法处理错误,请不要尝试恢复
blpfk2vs

blpfk2vs9#

不要使用panic来处理正常的错误。使用error和多个返回值。请参阅https://golang.org/doc/effective_go.html#errors。

相关问题