英文:
Avoid checking if error is nil repetition?
问题
我目前正在学习Go语言,我的一些代码看起来像这样:
a, err := doA()
if err != nil {
return nil, err
}
b, err := doB(a)
if err != nil {
return nil, err
}
c, err := doC(b)
if err != nil {
return nil, err
}
...以此类推...
这个代码对我来说看起来有点不对,因为错误检查占据了大部分行数。有没有更好的处理错误的方法?我能通过一些重构来避免这个问题吗?
更新: 感谢所有的回答。请注意,在我的例子中,doB依赖于a,doC依赖于b,以此类推。因此,大多数建议的重构在这种情况下都不适用。还有其他建议吗?
英文:
I'm currently learning go and some of my code looks like this:
a, err := doA()
if err != nil {
return nil, err
}
b, err := doB(a)
if err != nil {
return nil, err
}
c, err := doC(b)
if err != nil {
return nil, err
}
... and so on ...
This looks kinda wrong to me because the error checking takes most of the lines. Is there a better way to do error handling? Can I maybe avoid this with some refactoring?
UPDATE: Thank you for all the answers. Please note that in my example doB depends on a, doC depends on b and so on. Therefore most suggested refactorings don't work in this case. Any other suggestion?
答案1
得分: 61
这是一个常见的抱怨,有几个答案可以解决这个问题。
以下是几个常见的答案:
1 - 没有那么糟糕
这是对这些抱怨的一种常见反应。事实上,在你的代码中多出几行代码并不是那么糟糕。这只是一点点廉价的打字,当阅读代码时非常容易处理。
2 - 实际上是一件好事
这是基于这样一个事实:打字和阅读这些额外的行代码是一个很好的提醒,实际上你的逻辑可能会在那一点上逃逸,你必须撤销在之前的行代码中所做的任何资源管理。这通常与异常进行比较,异常可以以隐式的方式打破逻辑流程,迫使开发人员始终记住隐藏的错误路径。我之前写过一篇更深入的关于这个问题的抱怨文章在这里。
3 - 使用panic/recover
在某些特定情况下,你可以通过使用panic
和已知类型,在你的包代码进入世界之前使用recover
,将其转换为适当的错误并返回。这种技术最常用于展开递归逻辑,比如(un)marshalers。
我个人尽量不滥用这种技术,因为我更倾向于前两点。
4 - 稍微重新组织代码
在某些情况下,你可以稍微重新组织逻辑以避免重复。
举个简单的例子,原来的代码是这样的:
err := doA()
if err != nil {
return err
}
err := doB()
if err != nil {
return err
}
return nil
可以重新组织成:
err := doA()
if err != nil {
return err
}
return doB()
5 - 使用命名返回值
有些人使用命名返回值来从返回语句中去除err变量。不过我不建议这样做,因为它几乎没有节省什么,降低了代码的清晰度,并且使得逻辑容易出现细微的问题,当一个或多个结果在退出返回语句之前被定义时。
6 - 在if条件之前使用语句
正如Tom Wilde在下面的评论中提醒的那样,Go中的if
语句在条件之前接受一个简单的语句。所以你可以这样做:
if err := doA(); err != nil {
return err
}
这是一个很好的Go习惯,经常被使用。
在某些特定情况下,为了清晰起见,我更喜欢避免以这种方式嵌入语句,让它独立存在,但这是一个微妙而个人的问题。
英文:
This is a common complaint, and there are several answers to it.
Here are a few common ones:
1 - It's not so bad
This is a very common reaction to these complaints. The fact you have a few extra lines of code in your code is not in fact so bad. It's just a bit of cheap typing, and very easy to handle when on the reading side.
2 - It's actually a good thing
This is based on the fact that typing and reading these extra lines is a very good reminder that in fact your logic might escape at that point, and you have to undo any resource management that you've put in place in the lines preceding it. This is usually brought up in comparison with exceptions, which can break the flow of logic in an implicit way, forcing the developer to always have the hidden error path in mind instead. Some time ago I wrote a more in-depth rant about this here.
3 - Use panic/recover
In some specific circumstances, you may avoid some of that work by using panic
with a known type, and then using recover
right before your package code goes out into the world, transforming it into a proper error and returning that instead. This technique is seen most commonly to unroll recursive logic such as (un)marshalers.
I personally try hard to not abuse this too much, because I correlate more closely with points 1 and 2.
4 - Reorganize the code a bit
In some circumstances, you can reorganize the logic slightly to avoid the repetition.
As a trivial example, this:
err := doA()
if err != nil {
return err
}
err := doB()
if err != nil {
return err
}
return nil
can also be organized as:
err := doA()
if err != nil {
return err
}
return doB()
5 - Use named results
Some people use named results to strip out the err variable from the return statement. I'd recommend against doing that, though, because it saves very little, reduces the clarity of the code, and makes the logic prone to subtle issues when one or more results get defined before the bail-out return statement.
6 - Use the statement before the if condition
As Tom Wilde well reminded in the comment below, if
statements in Go accept a simple statement before the condition. So you can do this:
if err := doA(); err != nil {
return err
}
This is a fine Go idiom, and used often.
In some specific cases, I prefer to avoid embedding the statement in this fashion just to make it stand on its own for clarity purposes, but this is a subtle and personal thing.
答案2
得分: 8
你可以使用命名返回参数来简化代码。在Go语言中,命名返回参数可以让你的代码更简洁。
以下是示例代码:
func doStuff() (result string, err error) {
a, err := doA()
if err != nil {
return
}
b, err := doB(a)
if err != nil {
return
}
result, err = doC(b)
if err != nil {
return
}
return
}
在你使用Go语言编程一段时间后,你会意识到每个函数都需要检查错误,这让你思考如果函数出错了,它实际上意味着什么,以及你应该如何处理它。你可以点击这里查看示例代码的运行结果。
英文:
You could use named return parameters to shorten things a bit
func doStuff() (result string, err error) {
a, err := doA()
if err != nil {
return
}
b, err := doB(a)
if err != nil {
return
}
result, err = doC(b)
if err != nil {
return
}
return
}
After you've been programming in Go a while you'll appreciate that having to check the error for every function makes you think about what it actually means if that function goes wrong and how you should be dealing with it.
答案3
得分: 8
2023年的回答
现在,你可以使用errors
包中的辅助函数,例如errors.Join
来处理这些情况:
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if err := errors.Join(err1, err2); err != nil {
return err
}
如果你想要筛选出你想要单独处理的特定错误(而不仅仅是记录它们并进行一般性的错误处理),你可以使用errors.As
或errors.Is
:
func foo() error {
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if err := errors.Join(err1, err2); err != nil {
return err
}
}
err := foo()
if error.Is(err, fs.ErrNotExist) {
// 处理文件不存在的情况,例如打印特殊消息
// 但具体的错误无法直接访问,参见下面的`.As`方法
}
var notExistErr *fs.ErrNotExist
if error.As(err, notExistErr) {
// 只有在赋值可能的情况下才会执行到这里
// 现在可以使用`notExistErr`来提取有关`err`的信息
}
2013年的回答
如果你有许多这样的情况,需要进行多个错误检查,你可以定义一个类似下面的实用函数:
func validError(errs ...error) error {
for i, _ := range errs {
if errs[i] != nil {
return errs[i]
}
}
return nil
}
这样可以选择其中一个错误并返回,如果有一个非nil的错误。
示例用法(完整版本请参见playground):
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if e := validError(err1, err2); e != nil {
return e
}
当然,这只适用于函数之间没有依赖关系的情况,这是总结错误处理的一般前提条件。
英文:
Answer from 2023
Nowadays you would use helpers from the errors
package such as errors.Join
to handle these situations:
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if err := errors.Join(err1, err2); err != nil {
return err
}
If you want to filter for specific errors that you want to handle separately (instead of simply logging them away and do general fault handling), you can use errors.As
or errors.Is
:
func foo() error {
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if err := errors.Join(err1, err2); err != nil {
return err
}
}
err := foo()
if error.Is(err, fs.ErrNotExist) {
// handle fill not found by, e.g., printing
// a special message but concrete error cannot be
// accessed directly, see `.As` below for that
}
var notExistErr *fs.ErrNotExist
if error.As(err, notExistErr) {
// only reached if assignment is possible
// now `notExistErr` can be used to extract info about err
}
Answer from 2013
If you have many of such re-occurring situations where you have several of these
error checks you may define yourself a utility function like the following:
func validError(errs ...error) error {
for i, _ := range errs {
if errs[i] != nil {
return errs[i]
}
}
return nil
}
This enables you to select one of the errors and return if there is one which
is non-nil.
Example usage (full version on play):
x, err1 := doSomething(2)
y, err2 := doSomething(3)
if e := validError(err1, err2); e != nil {
return e
}
Of course, this can be only applied if the functions do not depend on each other
but this is a general precondition of summarizing error handling.
答案4
得分: 2
你可以创建一个带有结果值和错误的上下文类型。
type Type1 struct {
a int
b int
c int
err error
}
func (t *Type1) doA() {
if t.err != nil {
return
}
// 做一些事情
if err := do(); err != nil {
t.err = err
}
}
func (t *Type1) doB() {
if t.err != nil {
return
}
// 做一些事情
b, err := t.doWithA(a)
if err != nil {
t.err = err
return
}
t.b = b
}
func (t *Type1) doC() {
if t.err != nil {
return
}
// 做一些事情
c, err := do()
if err != nil {
t.err = err
return
}
t.c = c
}
func main() {
t := Type1{}
t.doA()
t.doB()
t.doC()
if t.err != nil {
// 处理 t 中的错误
}
}
希望对你有帮助!
英文:
You could create context type with result value and error.
type Type1 struct {
a int
b int
c int
err error
}
func (t *Type1) doA() {
if t.err != nil {
return
}
// do something
if err := do(); err != nil {
t.err = err
}
}
func (t *Type1) doB() {
if t.err != nil {
return
}
// do something
b, err := t.doWithA(a)
if err != nil {
t.err = err
return
}
t.b = b
}
func (t *Type1) doC() {
if t.err != nil {
return
}
// do something
c, err := do()
if err != nil {
t.err = err
return
}
t.c = c
}
func main() {
t := Type1{}
t.doA()
t.doB()
t.doC()
if t.err != nil {
// handle error in t
}
}
答案5
得分: 1
你可以将错误作为函数参数传递。
func doA() (A, error) {
...
}
func doB(a A, err error) (B, error) {
...
}
c, err := doB(doA())
我注意到在 "html/template" 包中有一些方法也是这样做的,例如:
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}
英文:
You can pass an error as a function argument
func doA() (A, error) {
...
}
func doB(a A, err error) (B, error) {
...
}
c, err := doB(doA())
I've noticed some methods in the "html/template" package do this e.g.
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}
答案6
得分: 0
你可能觉得这样做不对,可能是因为你习惯了不在调用处处理错误。这在Go语言中是相当惯用的,但如果你不习惯的话,看起来会有很多样板代码。
不过,这种方式确实有一些优点。
- 你必须考虑在生成错误的地方处理错误的正确方式。
- 阅读代码时很容易看到代码将在哪些地方中止并提前返回。
如果这真的让你感到困扰,你可以在for循环和匿名函数中发挥创意,但这通常会变得复杂和难以阅读。
英文:
It looks wrong to you perhaps because you are used to not handling errors at the call site. This is quite idiomatic for go but looks like a lot of boilerplate if you aren't used to it.
It does come with some advantages though.
- you have to think about what the proper way to handle this error is at the site where the error was generated.
- It's easy reading the code to see every point at which the code will abort and return early.
If it really bugs you you can get creative with for loops and anonymous functions but that often gets complicated and hard to read.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论