英文:
Should I use panic or return error?
问题
Go提供了两种处理错误的方式,但我不确定该使用哪一种。
假设我正在实现一个经典的ForEach
函数,它接受一个切片或映射作为参数。为了检查是否传入了可迭代对象,我可以这样做:
func ForEach(iterable interface{}, f interface{}) {
if isNotIterable(iterable) {
panic("应该传入一个切片或映射!")
}
}
或者
func ForEach(iterable interface{}, f interface{}) error {
if isNotIterable(iterable) {
return fmt.Errorf("应该传入一个切片或映射!")
}
}
我看到一些讨论说应该避免使用panic()
,但也有人说如果程序无法从错误中恢复,应该使用panic()
。
我应该使用哪一种?选择正确方式的主要原则是什么?
英文:
Go provides two ways of handling errors, but I'm not sure which one to use.
Assuming I'm implementing a classic ForEach
function which accepts a slice or a map as an argument. To check whether an iterable is passed in, I could do:
func ForEach(iterable interface{}, f interface{}) {
if isNotIterable(iterable) {
panic("Should pass in a slice or map!")
}
}
or
func ForEach(iterable interface{}, f interface{}) error {
if isNotIterable(iterable) {
return fmt.Errorf("Should pass in a slice or map!")
}
}
I saw some discussions saying panic()
should be avoided, but people also say that if program cannot recover from error, you should panic()
.
Which one should I use? And what's the main principle for picking the right one?
答案1
得分: 74
你应该假设 panic 会立即导致整个程序崩溃,或者至少导致当前 goroutine 崩溃。问问自己:“当发生这种情况时,应用程序是否应立即崩溃?” 如果是的话,使用 panic;否则,使用 error。
英文:
You should assume that a panic will be immediately fatal, for the entire program, or at the very least for the current goroutine. Ask yourself "when this happens, should the application immediately crash?" If yes, use a panic; otherwise, use an error.
答案2
得分: 18
使用panic
。
因为你的用例是捕获对你的API的错误使用。如果程序正确调用你的API,这种情况在运行时不应该发生。
实际上,任何使用正确参数调用你的API的程序在移除测试后将以相同的方式运行。测试只是为了在出现错误时提前失败,并向程序员提供有用的错误信息。理想情况下,当运行测试套件时,可能会在开发过程中达到一次panic
,程序员会在提交错误代码之前修复调用,这样错误的使用就永远不会进入生产环境。
另请参阅此回答对问题在Go中,使用错误进行函数参数验证是一种好的模式吗?。
英文:
Use panic
.
Because your use case is to catch a bad use of your API. This should never happen at runtime if the program is calling your API properly.
In fact, any program calling your API with correct arguments will behave in the same way if the test is removed. The test is there only to fail early with an error message helpful to the programmer that did the mistake. Ideally, the panic might be reached once during development when running the testsuite and the programmer would fix the call even before committing the bad code, and that incorrect use would never reach production.
See also this reponse to question Is function parameter validation using errors a good pattern in Go?.
答案3
得分: 6
我喜欢一些库中的做法,即在常规方法DoSomething
的基础上,添加了它的"紧急"版本MustDoSomething
。我对go
相对较新,但我已经在几个地方见过这种做法,特别是sqlx
库中。
总的来说,如果你想将你的代码暴露给其他人使用,你应该同时提供Must-
和常规版本的方法,或者你的方法/函数应该给客户端一个机会以他们希望的方式进行恢复,所以error
应该以go
的惯用方式对他们可用。
话虽如此,我同意如果你的API/库被不当使用,也可以使用panic。事实上,我也见过像MustGetenv()
这样的方法,如果关键的环境变量缺失,它会引发panic。基本上是一种快速失败的机制。
英文:
I like the way it's done in some libraries where on top of a regular method DoSomething
, its "panicky" version is added with MustDoSomething
. I'm relatively new to go
, but I've already seen it in several places, notably sqlx
.
In general, if you want to expose your code to someone else, you should either have Must-
and a regular version of the method, or your methods/functions should give the client a chance to recover the way they want and so error
should be available to them in a go
-idiomatic way.
Having said that, I agree that if your API/library is used inappropriately, it's Ok to panic as well. As a matter of fact, I've also seen methods like MustGetenv()
that will panic if a critical env.var is missing. Fail-fast mechanism basically.
答案4
得分: 3
如果在启动服务时没有提供或缺少某些必需的要求(例如数据库连接、某些必需的服务配置),那么应该使用 panic。
对于任何用户响应或服务器端错误,应该返回错误信息。
英文:
If some mandatory requirement is not provided or not there while starting the service (eg. database connection, some service configuration which is required) then you should use panic.
There should be return error for any user response or server side error.
答案5
得分: 2
请问以下问题:
- 无论你的应用程序编写得多好,你是否预计会发生异常情况?你认为在应用程序的正常使用过程中,让用户意识到这种情况是否有用?将其视为错误处理,因为它涉及应用程序的正常工作。
- 如果你适当地编写代码(并且有一定的防御性),那么这种异常情况是否不会发生?(例如:除以零,或者访问超出数组边界的元素)你的应用程序在出现这种错误时是否完全无能为力?恐慌。
- 你是否拥有自己的 API,并希望确保用户正确使用它?恐慌。如果错误使用,你的 API 很少能够恢复。
英文:
Ask yourself these questions:
- Do you expect the exceptional situation to occur, regardless how well would you code your app? Do you think it should be useful to make the user aware of such condition as part of the normal usage of your app? Handle it as an error, because it concerns the application as working normally.
- Should that exceptional situation NOT occur if you code appropriately (and somewhat defensively)? (example: dividing by zero, or accessing an array element out of bounds) Is your app totally clueless under that error? Panic.
- Do you have your API and want to ensure users use it appropriately? Panic. Your API will seldom recover if used incorrectly.
答案6
得分: 1
尽可能使用错误(error)
只有在代码可能进入糟糕状态且容易崩溃时,才使用 panic;也就是说,只有在真正意外的情况下。上面的 ForEach()
示例是一个公开的函数,它接受一个接口作为参数,因此应该预期会有人错误地调用它。如果它被错误地调用,你知道为什么无法继续执行,并且知道如何处理该错误。isNotIterable
是一个二进制值,很容易控制。
但错误(error)不像 try/catch
即使你试图通过查看其他语言中的 throw/catch 来证明 panic/recover 的合理性,你仍然应该使用错误(error)。我们知道你正在尝试调用该函数,因为你正在调用它,我们知道发生了错误,因为 err != nil
,就像检查抛出的异常类型一样,你可以使用 errors.Is(err, ErrNotIterable)
来检查返回的错误类型。
那么在并发中应该使用 panic 处理错误吗?
答案仍然很可能是否定的。在 Go 中,错误仍然是首选的方式,你可以使用等待组(wait group)来关闭 goroutine:
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
// 在 5 分钟后自动取消
defer cancel()
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
// 做一些疯狂的事情;你仍然可以检查错误
if ... {
return fmt.Errorf("critical error, stopping all goroutines now")
}
// 代码顺利完成
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())
}
}
但是错误看起来很冗长
是的,这就是重点。当返回错误时,此时可以对其进行处理。你给调用代码提供了选项,而不是决定开始关闭和终止应用程序,除非你使用 recover()
。如果你只是将同一个错误一直返回到调用堆栈的最上层,那么错误看起来确实不如 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
}
}()
...
// 也许是全局变量中的断开的管道
// 或者一个包含的模块向你抛出了 panic!
}
但如果你仍然不相信...这是 Go FAQ
我们认为将异常与控制结构耦合(如 try-catch-finally 惯用法)会导致代码混乱。它还倾向于鼓励程序员将太多普通错误(例如无法打开文件)标记为异常。
Go 采用了不同的方法。对于普通的错误处理,Go 的多值返回使得报告错误而不过载返回值变得容易。规范的错误类型,加上 Go 的其他特性,使得错误处理变得愉快,但与其他语言中的错误处理方式有很大不同。
英文:
Use error whenever possible
Only use panic when your code could end up in a bad state that would be prone to crashing; something truly unexpected. The example above with ForEach()
is an exported func that accepts an interface so it should expect someone will improperly call it. And if it is improperly called, you know why you cannot continue and you know how to handle that error. isNotIterable
is literally binary and easy to control.
But error is not like a try/catch
Even if you try to justify panic/recover by looking at throw/catch from other languages, you still use errors. We know you are trying the function because you are calling it, we know there was an error because err != nil
, and just like checking the type of exception thrown you can check the type of error returned with errors.Is(err, ErrNotIterable)
So should you use panic for errors in concurrency?
The answer is still most likely no. Errors are still the preferred way in Go and you can use a wait group to shut down the 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()
Even using the structure of the original example, you still have better control with errors than panics:
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())
}
But error feels very verbose
Yes, that is the point. When an error is returned, it is at that point to do something about it. You are giving the calling code options rather than making the decision to start shutting down and killing the application unless you recover()
. If you are just returning the same error all the way up the call stack then error will seem inferior to panic, but this is due to not addressing issues when they happen.
So when to use panic?
When your code is on a collision course to crash and you cannot assume your way out of it. Another is when the code assumes something that is no longer true and having to check the integrity in every function from here on out would be tedious (and might impact performance). Still, you would use panic()
only to get out of the layers of uncertainty... then still handle errors:
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!
}
But if you are still not convinced... 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.
答案7
得分: 0
恐慌通常意味着出现了意外错误。它主要用于在正常操作中不应该发生的错误或我们没有准备好妥善处理的错误上快速失败。所以在这种情况下,只需返回错误,而不要让程序发生恐慌。
英文:
A panic typically means something went unexpectedly wrong. Mostly used to fail fast on errors that shouldn’t occur during normal operation, or that we aren’t prepared to handle gracefully. So in this case just return the error, you don't want your program to panic.
答案8
得分: 0
我认为之前的回答都不正确:
- 默认情况下,如果我们不知道如何处理"error"代码,必须按照最佳编程模式进行panic:
https://en.wikipedia.org/wiki/Fail-fast
更正式地说,我们的"图灵机"出现了故障,我们需要返回到一个"稳定状态"或"重置状态"。更多信息请参见https://en.wikipedia.org/wiki/Reset_(computing)
例如,在Web(微)服务中,这意味着返回一个40X错误(由用户输入引起的panic)或50X错误(由其他原因引起的panic-硬件、网络、断言错误等)。
- 如果我们知道如何处理"error",那么首先我们就没有错误,而是一个不舒服的返回值。这是一种正常的执行条件,可能不是错误。通常,这对应于快乐路径与非快乐路径的建模。
总之,err返回值大多是一个错误的想法,即使GO社区已将其作为一种信仰采纳。使用错误返回值只是一种加快程序执行速度的补丁方式,因为它需要较少的CPU指令来实现,但大多数情况下,除了低级服务之外,它是无用的并且会促进糟糕的代码。(请注意,GO被设计为实现这些低级服务作为一种"易于C"的方式,但它被用于高级(Level 7)应用程序程序,当错误必须快速失败以避免继续进行可能导致资金损失或致命伤亡的未定义状态时。如果有疑问,默认情况下使用panic。
更新2023-03:(添加Erlang参考)没有关于处理错误的观点或惯用方法。处理错误有正确的方法和错误的方法。正确的方法是Erlang的方法(设计用于支持近乎100%的正常运行时间)。从intro中复制粘贴:
- 尽早崩溃:
- 让其他进程进行错误恢复
- 不要进行防御性编程
- 如果无法处理错误,不要尝试恢复
更新2023-05:(添加Helm参考)精心设计的Helm,虽然基于Go,但复制了每个定义函数的功能。默认情况下,函数会panic,当以must...为前缀时,它们会返回一个错误。REF。请注意,Helm用于配置高可用性的Kubernetes集群。
英文:
I think none of the previous answers are correct:
- By default, if we don't know what to do with the "error" code must panic following best programming patterns:
https://en.wikipedia.org/wiki/Fail-fast
Putting it more formally, our "Turing Machine" is broken and we need to come back to an "stable state" or "reset state". More info at https://en.wikipedia.org/wiki/Reset_(computing)
For example in web (micro)services that means returning a 40X error (panic caused by input from user) or 50X error (panic caused by something else - hardware, network, assert error, ...)
- If we know what to do with the "error", then we do not have an error in first place, but an uncomfortable return value. This is a normal execution condition and probably not an error. Normally this correspond to the happy vs non-happy path modeling.
In a summary, the err return value is mostly a wrong idea, even if the GO community has adopted it as a religion. Using error return values is just a patchy way to speed up program execution since it require fewer CPU instructions to be implemented, but most of the time, except for low-level services, it is useless and promote dirty code. (note that GO was designed to implement those low-level services as an "easy-C", but it was adopted for high-level (Level 7) application programs when an error must fail fast to avoid continuing with undefined states that can potentially cause money being lost of fatal casualties. In case of doubt, default to panic.
Update 2023-03: (Add Erlang reference) There are not opinionated or idiomatic ways to handler errors. There are correct ways and incorrect ways to handle errors. The correct way is the Erlang way (language designed to support ~100% of uptimes). C&P from intro
- Crash early:
- Let some other process do the error recovery
- Do not program defensively
- If you cannot handle the error, don’t try to recover
Update 2023-05: (Add Helm reference). The lovely designed Helm, while being Go based, duplicates the functionality of each defined function. By default, functions do panic, and when explicetely prefixed with must... they return an error. REF. Note that Helm is used to configure HA Kubernetes clusters.
答案9
得分: -1
不要在正常的错误处理中使用 panic。使用 error 和多个返回值。请参阅 https://golang.org/doc/effective_go.html#errors。
英文:
Don't use panic for normal error handling. Use error and multiple return values. See https://golang.org/doc/effective_go.html#errors.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论