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

huangapple go评论151阅读模式
英文:

Should a Go package ever use log.Fatal and when?

问题

我迄今为止一直避免使用log.Fatal,但最近我偶然发现了这些问题:code-coveragetests-using-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呢?

英文:

I have so far avoided use of log.Fatal, but I recently co-incidentally discovered these questions; code-coverage and tests-using-log-fatal.

One of the comments from the 100 code coverage questions says:

> ... In the vast majority of cases log.Fatal should be only be used in main, or init functions (or possibly some things meant to be called only directly from them)"

It go me thinking, so I began to look at the standard library code provided with Go. There are lots of examples where the test code in the library makes use of log.Fatal which seems fine. There are a few examples outside of the test code, such as in net/http, shown below:

// 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)
		}
    }
    ...
}

If its is best practice to avoid use of log.Fatal, why is it used at all in the standard libraries, I would have expected just return an error. It seems unfair to the user of the library to cause os.Exit to be called and not providing any chance for the application to clean-up.

I may be naive, so hence my question as a better practice would seem to be to call log.Panic which can be recovered and my theoretical long running stable application might have a chance of rising from the ashes.

So what would best-practise say for Go about when should log.Fatal should be used?

答案1

得分: 73

也许只是我个人的看法,但这是我使用log.Fatal的方式。根据UNIX的惯例,遇到错误的进程应尽早以非零的退出码失败。这导致我制定了以下使用log.Fatal的准则:

  1. ...在任何func init()中发生错误时,因为这些错误发生在导入过程中或在调用主函数之前。相反,我只做与库或命令行所要执行的工作单元直接相关的事情。例如,我设置日志记录并检查我们是否有一个合理的环境和参数。如果我们有无效的标志,没有必要运行主函数,对吧?如果我们不能给出适当的反馈,我们应该尽早告知。

  2. ...发生了一个我知道无法恢复的错误。假设我们有一个程序,根据命令行上给定的图像文件创建缩略图。如果该文件不存在或由于权限不足而无法读取,就没有继续执行的理由,也无法从这个错误中恢复。所以我们遵循惯例并失败。

  3. ...在一个可能不可逆转的过程中发生了错误。这是一个相对模糊的定义,我知道。让我举个例子来说明。假设我们有一个cp的实现,它被启动为非交互式,并递归地复制一个目录。现在,假设我们在目标目录中遇到一个与要复制到那里的文件具有相同名称(但内容不同)的文件。由于我们无法询问用户决定如何处理,并且无法复制此文件,我们遇到了问题。因为当我们以退出码为零完成时,用户会认为源目录和目标目录是完全相同的副本,所以我们不能简单地跳过这个文件。然而,我们也不能简单地覆盖它,因为这可能会破坏信息。这是一个用户明确请求无法恢复的情况,所以我会使用log.Fatal来解释这种情况,遵守尽早失败的原则。

英文:

It might be just me, but here is how I use log.Fatal. As per UNIX conventions, a process which encounters an error should fail as early as possible with a non-zero exit code. This lead me to the following guidelines to use log.Fatal when…

  1. …an error happens in any of my func init(), as these happen when the imports are processed or before the main func is called, respectively. Conversely, I only do stuff not directly affecting the unit of work the library or cmd is supposed to do. For example, I set up logging and check wether we have a sane environment and parameters. No need to run main if we have invalid flags, right? And if we can not give proper feedback, we should tell this early.
  2. …an error happens of which I know is irrecoverable. Let's assume we have a program which creates a thumbnail of an image file given on the command line. If this file does not exist or is unreadable because of insufficient permissions, there is no reason to continue and this error can not be recovered from. So we adhere to the conventions and fail.
  3. …an error occurs during a process which might not be reversible. This is kind of a soft definition, I know. Let me illustrate that. Let's assume we have an implementation of cp, and it was started to be non-interactive and recursively copy a directory. Now, let's assume we encounter a file in the target directory which has the same name (but different content) as a file to be copied there. Since we can not ask the user to decide what to do and we can not copy this file, we have a problem. Because the user will assume that the source and the target directories are exact copies when we finish with exit code zero, we can not simply skip the file in question. However, we can not simply overwrite it, since this might potentially destroy information. This is a situation we can not recover from per explicit request by the user, and so I'd use log.Fatal to explain the situation, hereby obeying the principle to fail as early as possible.

答案2

得分: 10

马库斯,我看到了你的回答,我认为它非常出色和富有洞察力,我倾向于同意你的分析。然而,要进行概括是非常困难的,尽管我一直在思考这个问题,作为一个对Go语言来说还是新手。我认为从理论上讲,无论是什么操作系统、包框架或库,日志记录器的责任只是简单地记录日志。在任何级别上,日志记录器的责任包括:

  • 格式化并一致地将信息打印到我选择的通道上。
  • 对不同的日志级别[调试、信息、警告、错误]进行分类、过滤和显示。
  • 处理和聚合异步和并行作业中的日志条目。

一个日志记录包或任何其他包不应该有权力崩溃程序,如果它正常运行的话。任何中间件或库都应该遵循抛出/捕获的模式,调用者有机会捕获所有抛出的异常。这也是一个很好的模式,在应用程序中构建基础和包来支持应用程序的各个部分,以及潜在的其他应用程序时,它们不应该直接崩溃应用程序。相反,它们应该抛出一个致命异常,让程序来处理。我认为这也解决了你提到的一些问题,因为它在未被捕获时立即通知调用者,作为一个致命崩溃。

在大多数情况下,我可以直接在Go语言中使用log.Fatal来处理直接面向用户的命令行工具,我认为这是它真正的简单性所在。但我不认为这是处理跨包的致命错误的长期方法。

英文:

Marcus, I came across your response, and I think its excellent and very insightful, and I tend to agree with your breakdown. It is very hard to generalize, though I have been thinking about it this a little more, and as a newbie to Go. I think on a theoretical level, if we are looking to best practices in computing, regardless of OS, package framework or library, a logger's responsibility is to simply log. On any level, a logger's responsibilities:

  • Format and print information consistently to the channel of my choosing.
  • Categorize, filter, display the different log levels [debug, info, warn, error]
  • Handle and aggregate log entries across async and parallel jobs

A logging package or any package does not and should not have authority to crash a program, if it is operating properly. Any middleware or library should follow a throw / catch pattern, with an opportunity for all exceptions thrown to be caught by the caller. This is also a good pattern to follow within an application, as you build foundations and packages that power various parts of your application, and potentially other applications, they should never crash an application directly. Rather, they should throw a fatal exception, allowing the program to handle. I think this also addresses some of your points Marcus, as it works to alert the caller immediately when uncaught, as a fatal crash.

For the most part I can enjoy leveraging log.Fatal in Go directly for the purposes of direct user facing cli tools, which I think is the simplicity it was truly intended for. I don't think it makes good sense as a long term approach to handle fatal errors across packages.

答案3

得分: 3

log.Fatal使用os.Exit,并且在错误是不可逆的且可能影响整个程序时最好调用。我认为log.Panic是一个更宽容的选项。

英文:

log.Fatal makes use of os.Exit and is best called when an error is irreversible and may affect the entire program. I think log.Panic is a more lenient option.

huangapple
  • 本文由 发表于 2015年11月24日 12:02:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/33885235.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定