超简短摘要:当出现错误时退出程序可能是个好主意。使用 gobail 会让您的生活更轻松。
当你的 go 代码出现错误时,你通常会看到类似这样的内容:
err := myfunc() if err != nil { return fmt.errorf("doing my thing: %w", err) }
您会在这个示例中注意到一些事情:
- 您必须检查是否有错误
- 有一些文本可以帮助诊断错误
- 错误被包装,与文本一起传回
那么接下来会发生什么?好吧,这一切又发生了。您检查错误值,描述它,然后将其传回。然后一切又重新开始。
这一切都取决于您正在编写的软件。当你遇到错误时,你必须做出决定。这个错误会发生什么?
如果您正在编写一个响应请求的 http api,那么最终您将获得某种 http 处理程序,并将该错误转换为某种响应 – 也许是一个带有礼貌提醒格式的小 400正确请求或可能返回 500 以及有关应用程序运行状况的令人担忧的消息。或者,如果您正在编写某种 cli 工具,那么您可能会决定错误最终会一直传递回您的主函数。对于任何类型的程序,您可能会认为已经足够了 – 该程序应该结束,因为您无法做任何其他事情。
让我们看看最后一个选项。什么时候退出程序合适?我能想到的几个原因:
a.没有别的办法了,错误太严重了,一切都必须立即停止
b.终止程序没有任何后果(不需要清理,没有状态可以响应)
c.尽早停止是可取的,也许你有一个监视器可以干净地重新启动该过程
无论什么原因,你都需要考虑如何干净利落地退出。现在您可能尝试的第一件事是:
err := myfunc() if err != nil { fmt.printf("doing my thing: %v", err) os.exit(1) }
它看起来与我们原来的错误处理代码非常相似,但有一些重要的区别。第一个是显而易见的——那里有一个很棒的stop-right-g*****n-now声明。你的程序不会继续下去。第二点也许更重要。调用此示例的代码不必担心处理任何错误。没有需要测试的其他代码路径 – 我们可以相信调用代码没有要测试的 if 块,因为没有返回任何需要我们检查的内容。
测试你的出口
所以,当我建议你应该相信你的退出代码会起作用时,我可能有点热情。您可能应该检查新程序停止的原因是否正确。
尝试 1 – 运行你的程序
这感觉应该很容易。这里有一些需要考虑的事情 – 在最简单的情况下,您只需运行程序并触发错误条件。例如,让 cli 工具打开一个不存在的文件。对于一些简单的情况,您可以手动执行此操作。当测试数量增加时,您可能需要某种自动化来帮助您。
快速旁注 – 这可能是另一篇博客文章的主题,但我目前最喜欢的测试 cli 工具的方法是使用 godog 来编写测试。它可能有点复杂,但我发现它非常强大。以下是我如何使用layli和wait-for来处理它的一些很好的例子。
这种方法会让您走得很远,但有时可能很难创造条件来正确执行您想要确信的所有代码路径。
尝试 2 – 模拟出口
好的,现在我们将使用 go 语言的一些功能。我们实际上不必调用 os.exit – 我们可以调用看起来像它的东西。所以看看这个:
type exitfunc func(code int) var customexit exitfunc = os.exit func myfunc() { err := someotherfunc() if err != nil { fmt.printf("doing my thing: %v", err) customexit(1) } }
那么我们如何利用这一点进行测试呢?由于函数现在已转换为变量 (customexit),因此我们可以用我们想要执行的其他操作替换该值。就像这样…
package sameastheabovecode func testomgitsallgonewrong(t *testing.t) { oldexit := customexit defer func() { // make sure we reset the exit so it can be used elsewhere customexit = oldexit } exitcalled := false exitcode := 0 customexit = func(code int) { exitcalled = true exitcode = code } // set up the mocks so that someotherfunc returns an error myfunc() // assume that we're using the fantastic stretchr/testify library here assert.true(t, exitcalled) assert.equal(t, 1, exitcode) }
这是一种对单元测试更加友好的方法。您可以检查使用的退出代码是否正确 – 并且您实际上调用了退出函数。
从表面上看,这看起来不错,但有一个大问题 – 如果您的测试通过,那么您的程序将继续并在您期望函数退出时执行该函数的其余部分。即使测试设置意味着其余的执行无效并导致测试出现问题(例如引起恐慌),它仍将继续。
删除代码以删除测试
嗯,这听起来有点极端!
我觉得我应该解释一下……通常在“管理良好”的公司中,您需要确保每一行代码都已被证明是有效的,然后才能放在客户面前。使用上述技术,您可能无法生成正确的覆盖率指标来证明您的能力良好。即使推理起来微不足道。
上面的所有示例都假设当我们收到错误时,我们必须检查它以决定做什么(报复性退出)。如果我们能够退出而无需检查是否存在错误,那不是很好吗?
让我们看看我们能做什么。
func checkexit(err error) { fmt.printf("doing my thing: %v", err) customexit(1) } func myfunc() { checkexit(someotherfunc()) }
看一下上面的例子。功能是相同的,但 myfunc 的实现现在更加简单 – 没有条件。我们可以在自己的测试中检查 checkexit 函数的实现,这意味着 myfunc() 中的任何新内容都可以更容易地验证。
戈贝尔介绍
创建了一个新的库 gobail,它可以让您确信如果出现错误,它将得到处理,而无需增加您自己的代码的复杂性。看起来像这样:
func myfunc() { gobail.run(someotherfunc()).orexit("with message") gobail.run(yetanotherfunc()).orexit("with message") }
这个库已经过全面测试,并具有覆盖率指标,以证明这一点。您可以安全地使用它,而不必担心错误会被跳过。它还将处理具有 2 个返回值的函数,如下所示:
gobail.Return2(return2ValsAndError()).OrExitMsg("something went wrong: %v")
另请注意,您包含了导致所有问题的错误。
也可以发生恐慌而不是退出,在调用恐慌时打印堆栈跟踪和程序中的其他上下文信息。请查看文档以了解更多详细信息。
隔离依赖关系
当您使用 gobail 编写软件时,您会注意到在与外部库交互时大多数情况下都必须使用它。这具有您通常需要编写来处理所有错误情况的附加代码,可以将其包装在对 return 或 return2 的调用中,并假设我们将在必要时退出。
结论
有时需要退出程序而不是详细处理错误。 gobail 库已创建并经过验证,因此您不必担心证明这一点的细节。
如果您发现可以进行的改进或只是有建议,请在存储库上提出 pr 或问题,开发人员会尽快处理!