【问题标题】:Avoid checking if error is nil repetition?避免检查错误是否是零重复?
【发布时间】:2013-09-17 06:30:10
【问题描述】:

我目前正在学习围棋,我的一些代码如下所示:

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,依此类推。因此,大多数建议的重构在这种情况下都不起作用。还有什么建议吗?

【问题讨论】:

  • 我完全同意你的观点,因为它是如此常见且如此重复,应该有一种简单的方法来做到这一点,比如在错误之前添加一个 @ 暗示此代码将执行默认错误检查, 所以你可以说:“c, @err := doC(b)” 编译器会为你插入重复的代码

标签: go


【解决方案1】:

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

以下是一些常见的:

1 - 还不错

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

2 - 这其实是件好事

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

3 - 使用恐慌/恢复

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

我个人尽量不过度滥用这一点,因为我与第 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 在下面的评论中很好地提醒的那样,Go 中的 if 语句位于条件之前的 accept a simple statement。所以你可以这样做:

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

这是一个很好的围棋成语,并且经常使用。

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

【讨论】:

  • 1.这不是 几行 行:他的大部分代码都是错误处理 [75%] 。 2:不是提醒,因为你很容易忘记 if(err) 并且你的错误会被吞没。 3:他不能使用panic,因为他不是doA()或file.create()等的作者。 4:你不能使用":="两次。 5:命名参数如何使代码更短? 6:如果doA()在检查错误后也返回了需要使用的结果怎么办?
  • 你的第 4 点完全破坏了你的第 1 点和第 2 点。如果你简写它,你就隐含地破坏了逻辑流程,迫使开发人员考虑 doB(a) 的可能结果。
  • 在所有示例中,您从未将函数结果分配给任何变量。或者,准确地说,如果您遵循约定,您将其分配给名为 err 的变量。
【解决方案2】:

你可以使用命名的返回参数来缩短一些东西

Playground link

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

【讨论】:

  • +1 - 我喜欢使用命名的返回值,但必须注意潜在的问题:在这个示例代码中它可以,但(未显示)result 可能设置在doA() 之间和doB(),如果doB() 失败,调用函数可能会对无效结果做一些事情。
  • 虽然我 100% 同意检查错误是一件好事,但没有理由需要如此冗长。例如,Rust 强制您检查任何返回 Result 类型的函数的错误,但有一个 ? 运算符只是“如果有错误立即返回”的语法糖。您有 1 行代码,而不是 4 行代码,而且您仍然必须显式处理每个错误。那些 'if err != nil' 块只是噪音。
  • @topskip 这就是为什么我认为调用者应该始终检查是否 err != nil 并且如果是则根本不依赖结果。
【解决方案3】:

如果您有许多此类重复发生的情况,其中您有几种情况 错误检查,您可以为自己定义一个实用函数,如下所示:

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

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

使用示例(full version on play):

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

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

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

【讨论】:

  • 谢谢!当几个独立的事情可能导致错误时,我会尝试这个。
【解决方案4】:

您可以创建带有结果值和错误的上下文类型。

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】:

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

    它确实有一些优点。

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

    如果它确实让您感到困扰,您可以使用 for 循环和匿名函数来发挥创意,但这通常会变得复杂且难以阅读。

    【讨论】:

      【解决方案6】:

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

      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
      }
      

      【讨论】:

      • var t = template.Must(template.ParseFiles("base.html", "domains.html"))
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-08-18
      • 2015-01-20
      • 2010-09-08
      相关资源
      最近更新 更多