likes
comments
collection
share

Error Flows in Go

作者站长头像
站长
· 阅读数 55

Go 的错误处理是一种讲故事的形式

原文链接:Go's Error Handling Is a Form of Storytelling

Error Flows in Go

我第一次看到 Go 代码时,就被大量的错误检查吓了一跳。Python 的口头禅之一是 "it’s easier to ask for forgiveness than permission",而我刚刚接触过 Python,看到到处都是这样的代码,我的眼睛被刺痛了:

if err != nil {
	return err
}

有那么一瞬间,我拒绝了现实,直接忽略了所有的错误,并试图用结果来凑合:

res, _ := getResultOrFail()
// bye, bye errors. I tricked the system. Or did I?

当然,随着时间的推移,我也被这个错误困扰过几次。Go 不是 Java,也不是 Python。在这些语言中,空指针异常虽然很烦人,但多少也是日常生活的一部分。当它发生时,一个线程会爆炸,但应用程序的其他部分会继续工作(除非它泄漏内存或导致死锁,但这是另一个话题)。在 Go 中,同样的情况会导致整个应用程序运行时崩溃。我认为这是一种令人耳目一新的方式,它教会你尊重错误处理,并尽可能减少运行时崩溃的机会。

还有一点--如果你的 Go 代码中有 80% 都是错误处理,那是因为你的代码中有 80% 随时都有可能出错。按照典型的契诃夫风格,如果你的代码有可能发生错误,那么它迟早会发生。

因此,我逐渐习惯了只添加一点处理代码。随着时间的推移,也多亏了自动键入工具,我学会了不用太担心操作的机械性。然而,通过编写:

if err != nil {
	return err
}

对我的代码帮助不大。我第一次检查错误日志时就意识到了这一点,结果是这样的:

ERROR: not found

等等--到底是什么东西没有找到?它是从哪里来的?这时我突然想到,Go 的错误处理之所以与众不同,很大程度上是因为它给了程序员讲述故事的机会。你看,如果你只是简单地向调用者返回一个错误,那就跟一开始就不返回错误差不多。在某些时候,这个错误会出现在调用堆栈中,有人会决定对它采取相应的措施,例如将它记录到文件中。不管是谁(很有可能是你)稍后查看该日志文件,都会因为得到这个什么也没说的隐晦提示而大为恼火。

正确讲述故事的诀窍在于尽可能为错误添加有意义的上下文。在 Go 中,添加错误上下文的字面意思就是在刚刚收到的错误信息中添加一些解释性文字,说明错误发生时你正在做什么。Go 中的错误类型是一个简单的接口,它公开了一个返回字符串的 Error() 方法。因此,出于实用的原因,Go 中的所有错误都可以等同于字符串(当然,如果你愿意,也可以让它们变得更复杂)。

我们可以使用 fmt 软件包中名为 Errorf 的便捷函数来代替返回错误。它接收一个格式化字符串,并用它生成一个新的错误。你传递给格式化字符串的参数不一定是错误本身,但我们强烈建议你这样做:

res, err := getResult(id)
if err != nil {
	return nil, fmt.Errorf("obtaining result for id %s: %w", id, err)
}

fmt.Errorf 有一个很好的特性,即如果在格式化字符串中使用 %w,新创建的错误实际上会封装原始错误(保留对它的内部引用)。如果稍后要检查该错误是否等同于一个众所周知的错误,这将非常有用:

if errors.Is(err, sql.ErrNoRows) {
	// do something
	// The condition above will match even if sql.ErrNoRows has been wrapped
	// multiple times over
}

撰写好的信息是关键

把错误信息看成是随时可以连接的东西。调用链上的某个人很可能会将错误打包,并预先添加他们的拼图。因此,您的信息最好简洁明了,并描述出错误发生时代码试图做什么。避免使用 failed、cannot、won't 等词语。- 这样,日志信息的读者就能清楚地知道,如果发生了这样的错误,就说明某些事情没有发生。下面就是一个很好的例子:

conecting to the DB

有人可能会将其包裹到调用者链中:

fetching order status: connecting to the DB

甚至更进一步:

tracking parcel location: fetching order status: connecting to the DB

读者可以通过上面这样一条信息来了解出错的原因--当用户试图追踪包裹位置时,数据库连接失败。这比下面的要清楚得多:

could not track location: unable to fetch order status: DB connection failed

甚至更糟的是:

error while tracking location: error while fetch order status: DB connection failed

下面是几个著名代码库中不太好的错误信息示例

错误上下文在代码中也能说明问题

多年前,作为一名新手开发人员,我喜欢在代码中到处添加注释,尤其是在可能导致异常或破坏预期逻辑的地方。那么,还有什么更好的办法能让这些注释变得有用,并将它们转化为错误信息上下文--因为它们就是错误信息上下文:

jobID, err := store.PollNextJob()
if err != nil {
	return nil, fmt.Errorf("polling for next job: %w", err)
}

owner, err := store.FindOwnerByJobID(jobID)
if err != nil {
	return nil, fmt.Errorf("fetching job owner for job %s: %w", jobID err)
}

j := jobs.New(jobID, owner)
res, err := j.Start()
if err != nil {
	return nil, fmt.Errorf("starting job %s: %w", jobID err)
}

// etc ...

在我看来,这不仅有助于你将来的自我调试,还能让你只需根据错误信息就能轻松浏览和理解冗长的代码。

Go 的错误流程

原文链接:Error Flows in Go

作者:Preslav Rachev

下面这篇文章是我在 2023 年写的一篇关于 Go 中错误处理的文章的后续。建议还没读过这篇文章的人先看看这篇文章。

您想过 Go 应用程序出错时会发生什么吗?从 Go 应用程序的角度来看,发生错误有三个不同点:

  1. 错误 "originates,",即首次出现(例如,您最后一次实际使用 errors.New)。
  2. 我们控制下的函数捕捉到错误,并决定采取以下措施之一
  • 缩短代码执行时间(panic, early return, clean up)
  • 或者更常见的做法是,将捕获到的错误与某些上下文进行封装,并将其返回给链中的前一个调用者。
  1. 错误会 "冒泡 "到最外层的调用者。除非途中发生了特殊情况(例如有人惊慌失措),否则这通常是 main 包,甚至是 main 函数本身。这通常是尝试窥探错误的地方,但老实说,在 99% 的情况下,这部分都很简单:

    1. 写出完整的错误信息,以便调试。
    2. 根据情况决定错误是否严重到需要继续运行应用程序。也许最好直接杀死应用程序,让基础架构重新启动。例如,如果错误发生在 HTTP 请求期间,这部分可能会向用户显示标准错误页面。但是,如果是在应用程序启动过程中发生的错误(例如,无法连接到数据库),我会直接 log.Fatal 该错误,然后让基础架构设置按顺序重启服务。

根据经验,除非您正在构建一个库,否则您可能不会花很多时间为您的应用程序引入新的错误。与其他 Go 应用程序一样,你的应用程序要么直接使用标准库,要么依赖于使用标准库的程序库。因此,在使用文件或数据库连接时,处理现有错误的几率要远远高于应用程序需要凭空编造新错误的几率。

不信?看看 Kubernetes 代码库中 errors.New 出现的次数与 fmt.Error(f) 出现的次数。多出两倍还多?

这就是我认为许多围棋开发者最大的误区。不,我说的不是这个:

if err != nil {
	return err
}

这简直毫无用处。我强调过用有用的上下文来包装所有错误的重要性,我仍然坚定地坚持这一点。我认为人们弄错的是上下文中的信息传递部分。我经常看到信息试图告诉我哪里出错了。相反,他们应该告诉我在事情搞砸之前他们试图做什么。

事实上,如果你们发现自己处于第 1 阶段,并返回了一个全新的错误,那么使用 "**** 出错了 "的说法是没有问题的:

return fmt.Errorf("DB failure: %w", err)

但你上面的 10 位调用者也会这样做。最后,不幸的值班开发人员得到的信息是这样的:

ERROR: DB failure: DB failure: DB failure: failed connecting to the DB: DB failure: "DB connection failed"

一点帮助都没有。你为什么不换个说法,告诉我代码在失败前试图做什么?换句话说,如果你在第 2 步,不要使用 "s*** got wrong"的模式,而是使用信息量更大的 "做了什么什么"。每个人都知道,如果他们看到错误日志,那是因为出错了,所以他们会多次感谢你没有一次又一次地使用 "failed"之类的词。

您可以查看我上一篇文章中的这一部分,以获得很好的说明。

转载自:https://juejin.cn/post/7380185173689532426
评论
请登录