Go 包是不是应该使用 log.Fatal 以及何时使用?
Posted
技术标签:
【中文标题】Go 包是不是应该使用 log.Fatal 以及何时使用?【英文标题】:Should a Go package ever use log.Fatal and when?Go 包是否应该使用 log.Fatal 以及何时使用? 【发布时间】:2016-02-26 09:31:46 【问题描述】:到目前为止,我一直避免使用log.Fatal
,但我最近偶然发现了这些问题; code-coverage 和 tests-using-log-fatal。
100 个代码覆盖率问题中的一个 cmets 说:
...在绝大多数情况下,
log.Fatal
应该只用在 main 或 init 函数中(或者可能某些东西只能直接从它们调用)"
这让我思考,所以我开始查看 Go 提供的标准库代码。有很多示例库中的 test 代码使用了log.Fatal
,这似乎很好。测试代码之外还有几个例子,比如net/http
,如下图:
// net/http/transport.go
func (t *Transport) putIdleConn(pconn *persistConn) bool
...
for _, exist := range t.idleConn[key]
if exist == pconn
log.Fatalf("dup idle pconn %p in freelist", pconn)
...
如果最好的做法是避免使用log.Fatal
,为什么在标准库中使用它,我预计只会返回一个错误。调用 os.Exit
并且不为应用程序提供任何清理机会似乎对库的用户不公平。
我可能很天真,因此作为更好的做法,我的问题似乎是调用log.Panic
可以恢复,并且我理论上长期运行的稳定应用程序可能有机会从灰烬中重生。
那么关于何时应该使用 log.Fatal,Go 的最佳实践会说什么?
【问题讨论】:
我希望这段代码绝对无法访问。导致空闲连接仍然在空闲列表中同时也被用作活动连接的错误不应该是永远应该发生的事情,并且发生应该是灾难性的。但由于他们似乎在空闲列表等周围正确使用互斥锁,我不知道为什么这个循环和代码甚至是必要的。为什么他们会立即进一步退出你的程序而不是恐慌是另一个谜。好问题。 该包中的任何测试是否能够到达该行? 好问题,稍微看了一下,看不到任何看起来像是专门为达到那条线而设计的东西。还没有定论... “偶尔在标准库中使用”与“最佳实践”并不冲突——一些标准。 lib 肯定是在最佳实践/习语出现之前编写的。我认为在任何 new 应用程序中,只有package main
应该调用 log.Fatal
。
我会说因为log.Fatal()
最后会调用os.Exit(1)
,所以你不应该这样做——改用普通的panic()
:如果它不会被捕获,应用程序将终止无论如何,打印出漂亮的堆栈跟踪,如果它被捕获,任何从包的代码中出现的恐慌都意味着所有的赌注都被取消了。所以恐慌只是为包用户提供了更大的处理空间。
【参考方案1】:
可能只有我一个人,但这就是我如何使用log.Fatal
。根据 UNIX 约定,遇到错误的进程应尽早失败,并使用非零退出代码。这引导我遵循以下准则,以便在……时使用 log.Fatal
。
-
…在我的任何
func init()
中都会发生错误,因为这些错误分别发生在处理导入时或调用主函数之前。相反,我只做不直接影响库或 cmd 应该做的工作单元的事情。例如,我设置了日志记录并检查我们是否有一个健全的环境和参数。如果我们有无效的标志,就不需要运行 main ,对吧?如果我们不能提供适当的反馈,我们应该尽早告知。
…发生了我知道无法恢复的错误。假设我们有一个程序可以创建在命令行中给出的图像文件的缩略图。如果此文件不存在或由于权限不足而无法读取,则没有理由继续,并且此错误无法恢复。所以我们遵守约定并失败了。
…在可能不可逆的过程中发生错误。这是一种软定义,我知道。让我来说明一下。假设我们有一个cp
的实现,它开始是非交互式的并递归地复制一个目录。现在,假设我们在目标目录中遇到一个与要复制到那里的文件具有相同名称(但内容不同)的文件。由于我们不能要求用户决定要做什么,也不能复制这个文件,所以我们遇到了问题。因为当我们以退出代码 0 结束时,用户会假设源目录和目标目录是精确的副本,所以我们不能简单地跳过有问题的文件。但是,我们不能简单地覆盖它,因为这可能会破坏信息。这是我们无法从用户的明确请求中恢复的情况,所以我会使用log.Fatal
来解释这种情况,在此遵循尽早失败的原则。
【讨论】:
我喜欢你的回答,因为它很好地解释了要点(并且与我自己在这个主题上的想法产生了共鸣)但我担心它错过了一个关键点:OP 明确询问使用log.Fatal
在一个包中——也就是说,在一段不受编写main()
的人控制的代码中。正如您所看到的,这实际上将问题从“行为良好的流程”领域转移到了“行为良好的包”领域——问题变成了完全不同的故事:是否可以不可逆地失败别人的程序?
它被覆盖了,虽然有点隐含:无论我写的是包还是 cmd,我都会应用这些规则:不可恢复的错误是不可恢复的错误。提供经过过滤的包输入是程序的责任。如果这不可能,则应返回错误,但前提是无法事先对输入进行清理。因此,在适当的情况下致命实际上有助于包的用户编写更好的代码。
事情是,如果某些东西是不可恢复的,它不是包维护者的电话。当然,您未能生成缩略图,但我将其用作 Web 服务器的一小部分。如果缩略图包因为无法加载文件而调用 os.Exit,我会被激怒。
嗯,这当然是真的,图片示例已根据命令行明确说明。到目前为止,我创建的 go 包仅用于我的工具,我在其中传递记录器。鉴于 log.Logger 不是一个接口并且 Logger 相当有限,我认为我不会在公共包中进行 any 日志记录(用户可能使用不兼容的记录器,例如 logxi)并使用返回的仅错误。 *对不让 Logger 成为接口的决定仍然摸不着头脑*
完全同意。致命的足够早地停止程序流以避免连续性【参考方案2】:
Marcus,我看到了您的回复,我认为它非常出色且非常有见地,我倾向于同意您的分解。很难一概而论,尽管我一直在考虑这个问题,并且作为 Go 的新手。我认为在理论上,如果我们正在寻找计算的最佳实践,无论操作系统、包框架或库如何,记录器的责任就是简单地记录。在任何层面上,记录者的责任:
按照我选择的渠道统一格式化和打印信息。 分类、过滤、显示不同的日志级别 [调试、信息、警告、错误] 跨异步和并行作业处理和汇总日志条目如果程序运行正常,日志记录包或任何包没有也不应该有权使程序崩溃。任何中间件或库都应遵循 throw / catch 模式,并有机会将抛出的 all 异常由调用者捕获。这也是在应用程序中遵循的一个很好的模式,当您构建为应用程序的各个部分以及可能的其他应用程序提供动力的基础和包时,它们永远不应该直接使应用程序崩溃。相反,他们应该抛出一个致命的异常,让程序来处理。我认为这也解决了您的一些观点,Marcus,因为它可以在未被捕获时立即提醒呼叫者,作为致命的崩溃。
在大多数情况下,我可以享受直接在 Go 中利用 log.Fatal 来直接面向用户的 cli 工具,我认为这是它真正想要的简单性。我认为作为一种处理跨包致命错误的长期方法没有什么意义。
【讨论】:
我同意@moniecodes:包不应该让你的程序崩溃,即使一个错误被认为是“不可恢复的”,因为一个包没有足够的上下文来知道调用者中还有什么需要清理的在出口之前。 @jgpawletko“崩溃”?出于正当理由停止处理并不完全是“崩溃”。【参考方案3】:log.Fatal 使用 os.Exit 并且最好在错误是不可逆的并且可能影响整个程序时调用。我认为 log.Panic 是一个更宽松的选择。
【讨论】:
以上是关于Go 包是不是应该使用 log.Fatal 以及何时使用?的主要内容,如果未能解决你的问题,请参考以下文章
Angular ui-router 中 $state.transitionTo() 和 $state.go() 之间的区别