问题 Go包是否应该使用log.Fatal以及何时使用?


到目前为止我一直避免使用 log.Fatal,但我最近偶然发现了这些问题; 代码覆盖 和 测试 - 使用日志致命

100个代码覆盖问题中的一条评论说:

......在绝大多数情况下 log.Fatal 应该只用在main或init函数中(或者可能只是一些只能直接从它们调用的东西)“

它让我思考,所以我开始查看Go提供的标准库代码。有很多例子 测试 库中的代码可以使用 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 哪些可以恢复,我理论上长期运行稳定的应用程序可能有可能从灰烬中升起。

那么什么是最佳实践说Go应该什么时候应该使用log.Fatal?


4215
2017-11-24 04:02


起源

我希望这段代码绝对无法访问。导致空闲连接仍然在空闲列表中同时也被用作活动连接的错误不应该是应该发生的事情,并且它发生应该是灾难性的。但是因为它们似乎在空闲列表中正确使用互斥锁等等,所以我不知道为什么这个循环和代码甚至是必要的。为什么他们会立即进一步退出你的计划而不是慌张是另一个谜。好问题。 - captncraig
该软件包中的任何测试都能够达到该范围吗? - captncraig
好问题,有一点看,不能看到任何看起来像它专门设计到达那条线的东西。还没确定...... - miltonb
“偶尔在标准库中使用”与“最佳实践”不冲突 - 某些标准。在最佳实践/习语出现之前,lib肯定是在写的。我会争辩说 新 申请,只有一个 package main 应该打电话 log.Fatal。 - elithrar
从那以后我会说 log.Fatal() 电话 os.Exit(1) 最后,你不应该这样做 - 使用普通 panic() 相反:如果它不会被捕获,应用程序将终止,打印出好的堆栈跟踪,如果它被捕获,任何从包的代码中出现的恐慌意味着所有的赌注都是关闭的。因此恐慌只是为包装用户提供了更大的处理空间。 - kostix


答案:


它可能只是我,但这是我如何使用 log.Fatal。根据UNIX约定,遇到错误的进程应尽早使用非零退出代码失败。这导致我使用以下指南 log.Fatal 什么时候…

  1. ......我的任何一个错误都会发生 func init(),因为这些都是在处理导入时或在调用主func之前发生的。相反,我只做不直接影响库或cmd应该做的工作单元的东西。例如,我设置了日志记录并检查我们是否有一个理智的环境和参数。如果我们有无效标志,则无需运行main,对吧?如果我们无法提供适当的反馈,我们应该尽早说明。
  2. ......发生了一个我知道无法恢复的错误。假设我们有一个程序可以创建命令行上给出的图像文件的缩略图。如果此文件由于权限不足而不存在或不可读,则没有理由继续,并且无法从中恢复此错误。所以我们遵守惯例而失败。
  3. ...在一个可能不可逆的过程中发生错误。我知道,这是一种软性定义。让我来说明一下。我们假设我们有一个实现 cp,它开始是非交互式的,并递归地复制目录。现在,让我们假设我们在目标目录中遇到一个文件,该文件具有相同的名称(但内容不同)作为要复制的文件。由于我们不能要求用户决定做什么而我们无法复制此文件,因此我们遇到了问题。因为当我们完成退出代码零时,用户将假定源目录和目标目录是精确副本,我们不能简单地跳过有问题的文件。但是,我们不能简单地覆盖它,因为这可能会破坏信息。这是我们无法从用户的明确请求中恢复的情况,因此我将使用 log.Fatal解释情况,特此遵守尽早失败的原则。

10
2017-11-24 09:40



我喜欢你的答案,因为它很好地解释了这些要点(并且我对这个主题的想法产生了共鸣)但我担心它错过了一个关键的一点:OP明确询问使用 log.Fatal  在一个包中  - 也就是说,在一段不受任何人写作控制的代码中 main()。正如您所看到的,这实际上将问题从“行为良好的过程”域转移到“行为良好的程序包”域 - 问题的完全不同的故事:是否可以不可逆转地使其他人的程序失败? - kostix
虽然有点含蓄,但它有所涵盖:无论我编写包还是cmd,我都应用这些规则:不可恢复的错误是一个不可恢复的错误。提供包装消毒输入是一项计划责任。如果不可能,则应返回错误,但前提是无法对输入进行清理。因此,在适当的情况下致命实际上有助于包的用户编写更好的代码。 - Markus W Mahlberg
事情是,如果某些东西不可恢复,那么它不是包维护者的电话​​。当然,您无法生成缩略图,但我使用它作为我的Web服务器的一小部分。如果缩略图包调用os.Exit因为它无法加载文件,我会被激怒。 - captncraig
嗯,这当然是正确的,图片示例是根据命令行明确说明的。我到目前为止创建的go包仅用于我的工具,我在其中传递记录器。鉴于log.Logger不是一个接口而Logger相当有限,我认为我不会这样做 任何 登录公共包(用户可能使用不兼容的记录器,如logxi)并仅处理返回的错误。 *仍然在决定不让Logger成为界面* - Markus W Mahlberg