Go程序结束时会发生什么? (带字幕的播客)

2021-02-10 01:10:38

您好,欢迎来到Go Time。我是Mat Ryer。今天我们在谈论Go程序结束时会发生什么。 func main返回时会发生什么。可能仍在运行的goroutine会发生什么,或者…还记得那些延迟的语句吗?他们怎么了?他们怎么走?那打开文件呢?他们会为我关闭吗,还是我必须这样做?那些HTTP响应主体又如何呢?我们应该关闭它们;每个人都记得关闭响应机构,但是当您退出响应机构时会发生什么呢?好吧,这里有很多问题,我们将在这个超深入的法医分析细分版(超酷)中找到所有问题的答案,我们将研究Go程序结束时发生的情况。是的,这是一个非常戏剧性的介绍,可能是一个非常平凡的话题,但我认为这不会。

您之前曾告诉我您从未经历过Go程序结束,所以这是未知的–

[]我没那么说……[笑]我说我的大多数程序并不是为了结束而设计的。因此,当它们结束时,我正在尝试确保服务器将其备份。

对。有趣。好的,我敢肯定,我们会谈更多。他告诉我,Go团队的成员也加入了其中,他在过去2.3年中一直致力于运行时。欢迎来到展览,Michael Knyszek。你好!

好的。它们是类似的东西。好的,让我们开始吧,也许就在刚开始的时候,对于刚接触Go的人,会发生什么–我的意思是,最终,最后一个程序将停止运行...那么在那里发生了什么?这是怎么回事?

好吧,我想基本上Go本身就是这样–这将以错误的方式出现;希望我们会做得更深入,并解释原因和原因,但是Go只会给您带来混乱,并直接调用操作系统,就像“我们完成了所有事情”一样。一切都死了,被清理了。操作系统运行并清理所有内容,并且如果正在运行的Go程序具有父进程,那么,在Linux上,所有进程都具有父进程……然后它将给该父进程一个返回代码。在Linux上,我相信只有一个介于0到255之间的值。出于兼容性的原因,Go是os.Exit –好吧,我不会进入os.Exit,但是Go基本上默认返回零,这意味着一切都很好。有效地返回非零值意味着出现了问题。有些程序喜欢对不同的含义使用不同的数字,但总的来说,这就是模式……只是,一切都很好,而且出了问题。

好的,太好了。它们像HTTP状态代码一样,是退出代码吗?是否有任何标准,或者仅仅是零就意味着成功,然后程序定义了其他所有内容?

我认为这是您唯一可以依靠的东西。如果您要处理的是特定程序,例如您正在为其编写包装器脚本,并且想要[无法理解]出现另一条错误消息,或者将其记录在某个地方,那么它会很有用。我觉得我知道一些程序可以在一个大表中定义所有不同值的含义……但是我认为通常来说,您可以依靠的唯一东西是零或非零。

对。因此,在Go中,一个主函数会在返回时–没有return参数,因此它只是通过退出代码块的后面而返回。那默认情况下会返回零,是吗?

然后,如果您确实想返回非零值,那就是我们需要查看os.Exit的时候。

好的,我们稍后再讨论……但是您提到了操作系统已清除了所有内容,而Go有点混乱……在那里专门清除了哪些内容?

基本上,Go向操作系统请求一堆内存。最明显的是收集所有的内存。回收应用程序中[unintelligible]的所有与内存相关的资源。其他事情包括是否有任何打开的文件句柄……因此,它的范围很广。但是在简单的情况下,您仅在本地硬盘上有一个文件,或者您通常认为的文件-基本上,操作系统会为您关闭该文件句柄。它会跟踪所有这些文件,一旦您的程序退出,它就会遍历所有这些文件,并说:“好的,此过程不再使用此文件。”

那太酷了回收内存是一件很不错的事情……那么,如果我们有一些程序具有庞大的数据映射,那么在返回之前,我们不必遍历并删除所有这些数据,对吧?我们不必去做这种清理,释放内存。那会自动发生,对吗?是的

[]然后是一个有趣的文件。如果您在Go中打开文件,通常我们会推迟该文件的关闭,否则我们可能会有其他机制来关闭该文件……如果您不关闭该文件而程序退出,是否会泄漏文件句柄,或者操作系统会清理吗?

否,操作系统会对此进行清理。大多数系统上的文件都很漂亮–文件的概念已深入操作系统。实际上,它只是跟踪这些事情并说“好,此过程已退出”,并且如果我记得[无法理解],它通常会为这些文件保留引用计数。

当然。但是,如果您使该代码处于循环中或类似的状态,那么记住随时随地关闭文件很重要。您不能依赖该程序的结尾。是的,非常酷。乔恩,您通常如何退出并处理程序中的取消事项?如果您运行命令行工具,您将如何做?

您看到的最常见的方式是使用上下文并以这种方式对其进行处理……但是我可以肯定地说,我一直以来都没有这样做,这是我的内,尤其是当它–您知道,如果我只是在编写一个快速工具对于我自己还是其他人,我不希望它花费很长时间,如果它只能获取三个文件,快速地真正解析它们,然后执行某些操作并完成,通常,如果我想取消它,则程序会在无论如何它都结束了,所以实际上并没有太大的区别。

现在,如果我有更长时间的工作,那么也许更有意义。我猜这取决于您在做什么,以及停止某件事的中间是否真的很糟糕……所以这就像对我来说是决定性的因素,它是否真的停下来真的很重要。

是的,这很有趣-该程序是否停止才重要。您可以想象程序-我最近写了一个正在处理文件的程序,它将打开另一个文件,基本上从第一个文件生成一些数据。因此,它将为找到的每个文件创建一个新文件。因为这是一件小巧的事情,所以它依赖于该文件的状态来查看其是否已被处理。因此,在这种情况下,如果程序只是在某个地方的所有地方结束,那么我可能会处于磁盘上的状态,这是不希望的,并且无法反映现实。这也导致我们谈论正常关闭,在这里我们注意到某个程序想要结束,或者操作系统或某人想要结束该程序,但是我们之前需要做一些工作……因此,我们的解决方案是什么?做这样的事情?我们怎么知道该程序将要结束,又该如何做一些工作呢?

程序可以真正结束–从广义上讲,我可以以两种方式结束。要么告诉程序结束,要么决定“我完成了”并自行关闭。在外部环境中,您可能会看到类似Ctrl + C的窗口。如果您在命令行中输入Ctrl + C,则基本上会发生Linux发送所谓的信号的情况,这令人惊讶地难以在Go之外正确使用。 Go实际上使它非常好用,因为它将整个内容包装在一个通道中。但是一旦程序接收到信号,就需要以某种方式进行处理。因此,使用Go,您可以使用os / signal包来获取有关何时获得Ctrl + C之类的通知。某人想结束您的程序,因此使用os / signal包可以使您捕获并说:“好吧,让我进行所需的清理,以使我能够正常关机。”

[]如果程序要在内部结束,则有更多的假设,即程序作为一个整体会知道这一点,并且如果要正常关闭,则必须提供自己的机制来完成。

对。那讲得通。那么那里的运行时代码是否很混乱?因为我想它正在处理许多极端情况,并且有许多不同的操作系统,对吗?

好吧,信号处理非常困难,因为信​​号处理程序几乎可以在任何时间在任何线程上运行。当您正好拿着几把锁的中间时,一个信号会降落,并且您就像“好吧,做任何事安全吗?”是的,运行时的那部分实际上非常棘手,很难正确处理。它也是操作系统的一个复杂部分。

Go小组的奥斯汀(Austen)在Linux内核中发现了与Go 1.14发行周期中的信号有关的错误……所以,这很艰难。

是的,这确实是旧技术,不是吗?因为它是真正的核心,所以在其中确实很深。

是啊是啊。但是信号包确实为您提供了一个非常不错的包装器。与常规信号处理程序相比,它非常安全,并且使用起来更容易。

因此,假设我正在跳入这一步,并且想弄清楚如何捕获信号……我是否需要学习一堆不同的信号?如果有人在Linux终端中使用kill来删除进程,而不是Ctrl + C,而是使用多种方法来尝试停止程序…还是这种选择是从一个或两个信号中去除?如果有人想从这里开始,该从哪里开始?

我认为os / signal软件包文档确实很好地描述了它们之间的区别。您提到杀死很有趣,因为如果我没记错的话,杀死只是您根本无法捕捉到的信号之一。那就是杀死的危险-如果将杀死发送到进程,它将永远没有机会进行清理。这就像是强制退出强制退出。没有机会。

我知道的其他两个是SIGINT,因此是中断,即Ctrl + C。 SIGABRT很有意思,因为这将导致Go运行时基本上转储一堆goroutine堆栈跟踪…但是SIGABRT是另一个有时对显式处理有用的代码。但是Ctrl + C是最大的功能。

我确实认为os / signal软件包为此提供了一些很好的文档,因为它还包裹了您拥有许多不同的事实– Go支持许多不同的平台;当然,这在Windows和其他东西上的工作方式会稍有不同。因此,我遵循os / signal文档中的精确语义。

很公平。从Go 1.16开始,我们实际上在信号包中也有一个NotifyContext帮助器……它将取消信号上的上下文。这有点不错。如果您要在整个程序中使用上下文进行取消-这本质上是不熟悉任何人的模式,您可以在所有程序链中将上下文参数作为第一个参数传递,然后在循环内进行传递工作,或者正在遍历一组数据,您可以定期进行操作,即在每个循环的开始,检查该上下文是否已完成,并且有一个要关闭的通道,或者您可以检查是否返回了错误。然后您可以中止该操作。因此,这是进行正常关机的一种好方法,或者至少是“我将完成当前正在执行的操作,然后再停止操作。”它为您提供了一种正常的关闭方式,并且您可以通过上下文很好地做到这一点。但是您曾经不得不自己编写该信号代码。加上NotifyContext,您就不需要了。您可以将其连接到上下文,并且在程序中断时将为您取消它。

[]我认为这是一个好习惯-这是我一直在做的事情...如果您收到第二个中断信号,则值得更认真地退出。有时,我认为操作系统会将秒杀作为第二个信号发送。但是,如果这只是命令行,并且按Ctrl + C组合键,但逻辑上某处出现问题,则很容易挂起,因为您已捕获到该信号。因此,寻找第二个人并立即退出操作系统是一种好习惯,这样一来,您就不必陷入强迫退出自己的生意的困境。是的,我认为正常关机非常酷。

获得某种形式的正常关机或至少在您执行清理操作后的另一种方法是使用defer语句。在func主函数中,当您将其中的内容延迟时,它们确实会在函数退出之前(因此在程序退出之前)被调用。但这对os而言并非如此。出口,迈克尔吗?

不,所以OS.Exit是一个硬出口。它基本上完成了最少的清理工作,这对于Go运行时基本上意味着,如果您在启用了种族检测器的情况下运行,它将使用种族检测器进行一些清理,因此请尝试发出“哦,如果您例如,有一个恶意程序,它将确保其退出代码不为零。但是否则-是的,它基本上只是硬性退出。它不会尝试运行延迟的功能;如果您知道这些终结器,也不必费心尝试运行终结器。有点阴暗的角落,但值得一提。

是。好吧,os.Exit是一个非常紧急的停留,您不会拥有Go会给您带来的好处。您必须牢记这一点。

有趣的是,标准输入和输出流会发生什么,以及标准错误?例如,仅标准输出会在末尾收到io.EOF吗?它会做些关闭管道的事情吗?那里到底发生了什么?那也取决于操作系统吗?

这可能与系统有关。我考虑的是Linux / Unix理念,管道只是文件。对于操作系统,它使用相同类型的资源,[无法理解],并且这些标准输出,标准错误,标准输入-它们以与任何其他文件完全相同的方式关闭。

我将注意到,无论您是否执行代码,执行此类退出调用的那一刻都是悬而未决的。某些Go代码可能会在该进程停止运行之前的几毫秒内运行,或者它的线程停止了……但是您不能依靠它。因此,没有EOF传播出去,因为没有任何代码可以处理io.EOF(如果可行)。该代码不能保证完全运行。

因此,当我们称之为os.Exit时,您可以假设从那时起就像有人刚走开一样,无论发生什么事,都发生了,但是在某个时候一切都翻滚了……

这实际上是返回非零退出代码的唯一方法,不是吗?

那时很有趣……所以您必须小心一点。但是您可能希望您的程序以特定的状态代码退出。但是,如果您正在程序的某个深处进行操作,则可能未发生其他事情……因此,您可能只想使用os.Exit,就在主程序顶部或附近,基于从您作为应用程序一部分创建的其他函数返回。

[]是的,通常这是一个很好的模式。基本上,我所看到的是您拥有main,如果您从main干净地返回,那就是零出口...因为有趣的是,如果您在引擎盖下查看,从main返回时,它所做的只是很小的事情清理一下,这就是种族检测器的东西……然后它调用相同的退出系统调用。它与os.Exit完全相同。

所以这也是退出的正确点,因为这基本上就像在说:“好吧,如果我返回main,它只会调用os。有效退出零,所以现在是运行os的好点。退出一个。”话虽如此,这取决于程序。我当然可以想象一个程序,当您感觉到“我无法继续。即使其他事情仍在进行,我也绝对无法继续。也许把所有东西都扔在地板上是有意义的。”

是的对于这种情况,我们在Go中会感到恐慌。这太有趣了。那时恐慌本身就是一个很有趣的情况,因为它们可能会出现在程序中的任何地方……而且,如果未被捕获,它们会导致程序终止。但是,延误确实会引起恐慌,不是吗?我们知道,因为这是您从恐慌中恢复的方法-您可以在defer函数中运行代码。

恰恰。恐慌将导致延误,实际上,这并不是唯一要推迟的事情。如果您执行runtime.Goexit,就像goroutine调用runtime.Goexit一样,它也会执行其延迟。这是完全安全的,因为goroutine本身基本上是同步的–我们知道我们现在要停止执行goroutine,并且我们有点后退并运行所有延迟。

因此,如果您正在为goroutine执行runtime.Goexit,那么我假设您没有像使用os.Exit调用那样具有相同的清除保证……例如,您怎么说所有文件和所有其他内容操作系统得到处理。我假设没有单独跟踪goroutines文件。

不,不。那是在低得多的水平上进行的。如果一个goroutine退出了-当然,除非最后一个goroutine退出-但这并不能说明程序可能要建立的其余资源。

是的,这很有趣,当您考虑诸如HTTP响应正文之类的内容时,当您了解其中之一时,请务必仔细阅读。如果使用HTTP客户端发出请求,则会得到该消息;您得到响应,并且该响应可能有也可能没有身体。我们负责关闭这些主体以清理内存和其他东西。大概,如果程序结束,对我们来说就是这样,诸如此类的事情……因为它们某种程度上依赖底层操作系统来管理资源,对吗?

[ ] 对。同样,在Unix哲学中,“一切都是文件。 “ Internet连接也是如此,TCP / IP连接也是如此”,它是所有HTTP的基础–它是大多数操作系统直接构建在操作系统中的骨干结构,并且通常通过看起来像套接字的接口公开… Go中的接口看起来像[unintelligible],表示基础连接。因此,基本上,如果您使用os.Exit,它将像其他任何文件一样关闭该套接字。因此,如果您的另一端有一个客户端在监听该连接,则就像连接突然终止一样。因此,这是相同的故障模式。

关于其中一些的最酷的事情是,如果您去编写一个只有网络服务器的小程序,然后坐在那里睡觉十秒钟,然后弯腰进入其中或只是建立连接,就可以实际测试它们。然后关闭服务器,看看发生了什么,您可以看到发生了什么。

是的就像,如果您只是使用curl作为客户端来连接到服务器,并且您正在运行本地主机或其他设备,并且服务器就像在响应之前先睡眠10秒钟,然后按住Ctrl + C或在实际完成之前将其杀死,您可以看到[无法理解]

相当酷的API –只是一种宁静的正念。不是RESTful的,而是只睡觉的正念;一个刚刚睡觉的小API。我认为这是个好主意,尤其是在当今世界,一切都在快速发展,就像在电影中一样。

这是完美的。人们称它为Web请求是否超时。

是的,你去了。这真好。迈克尔,你怎么样

......