74

我目前正在学习 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
}
... and so on ...

这对我来说看起来有点不对,因为错误检查占用了大部分行。有没有更好的方法来进行错误处理?我可以通过一些重构来避免这种情况吗?

更新:谢谢你的所有答案。请注意,在我的示例中,doB 取决于 a,doC 取决于 b,依此类推。因此,大多数建议的重构在这种情况下都不起作用。还有什么建议吗?

4

6 回答 6

50

这是一个常见的抱怨,有几个答案。

以下是一些常见的:

1 - 还不错

这是对这些投诉的一种非常普遍的反应。您的代码中有几行额外的代码实际上并没有那么糟糕。这只是一些廉价的打字,在阅读方面很容易处理。

2 - 这实际上是一件好事

这是基于这样一个事实,即键入和阅读这些额外的行是一个很好的提醒,实际上您的逻辑可能会在此时逃脱,并且您必须撤消您在其前面的行中实施的任何资源管理。与异常相比,这通常会被提出,异常会以隐含的方式破坏逻辑流程,从而迫使开发人员始终牢记隐藏的错误路径。前段时间我在这里写了一篇更深入的咆哮。

3 - 使用恐慌/恢复

在某些特定情况下,您可以通过使用已知类型来避免其中的一些工作panic,然后recover在您的包代码发布之前使用,将其转换为适当的错误并返回它。这种技术最常用于展开递归逻辑,例如 (un) marshaler。

我个人尽量不过度滥用这一点,因为我与第 1 点和第 2 点的关联更紧密。

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 - 使用命名结果

有些人使用命名结果从 return 语句中去除 err 变量。不过,我建议不要这样做,因为它节省的资源很少,降低了代码的清晰度,并且当在 bail-out return 语句之前定义了一个或多个结果时,逻辑容易出现微妙的问题。

6 - 在 if 条件之前使用语句

正如 Tom Wilde 在下面的评论中提醒的那样,ifGo 中的语句在条件之前接受一个简单的语句。所以你可以这样做:

if err := doA(); err != nil {
    return err
}

这是一个很好的 Go 习惯用法,并且经常使用。

在某些特定情况下,我宁愿避免以这种方式嵌入声明,只是为了让它独立起来,以便清楚起见,但这是一件微妙而个人化的事情。

于 2013-09-12T18:53:01.340 回答
6

您可以使用命名的返回参数来缩短一些事情

游乐场链接

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 编程一段时间后,您会意识到必须检查每个函数的错误会让您思考如果该函数出错的实际含义以及您应该如何处理它。

于 2013-09-12T19:00:36.740 回答
4

如果你有很多这样的重复发生的情况,你有几个这样的错误检查,你可以为自己定义一个效用函数,如下所示:

func validError(errs ...error) error {
    for i, _ := range errs {
        if errs[i] != nil {
            return errs[i]
        }
    }
    return nil
}

这使您可以选择错误之一,如果有一个非零则返回。

示例用法(完整版播放):

x, err1 := doSomething(2)
y, err2 := doSomething(3)

if e := validError(err1, err2); e != nil {
    return e
}

当然,这只能在函数不相互依赖的情况下应用,但这是总结错误处理的一般前提条件。

于 2013-09-12T20:20:27.443 回答
2

您可以使用结果值和错误创建上下文类型。

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
    }

}
于 2018-04-29T08:19:33.970 回答
0

这对您来说可能是错误的,因为您习惯于不在呼叫站点处理错误。这对于 go 来说是非常惯用的,但如果你不习惯它,它看起来就像很多样板。

不过,它确实具有一些优势。

  1. 您必须考虑在生成错误的站点上处理此错误的正确方法是什么。
  2. 阅读代码很容易查看代码中止和提前返回的每个点。

如果它真的给你带来了错误,你可以使用 for 循环和匿名函数来发挥创意,但这通常会变得复杂且难以阅读。

于 2013-09-12T18:58:14.600 回答
0

您可以将错误作为函数参数传递

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
}
于 2016-06-24T01:17:37.867 回答