【问题标题】:Go destructors?去破坏者?
【发布时间】:2015-12-22 11:07:27
【问题描述】:

我知道 Go 中没有析构函数,因为从技术上讲没有类。因此,我使用initClass 来执行与构造函数相同的功能。但是,有没有办法在终止时创建一些东西来模仿析构函数,例如关闭文件?现在我只打电话给defer deinitClass,但这是相当骇人听闻的,我认为这是一个糟糕的设计。正确的方法是什么?

【问题讨论】:

  • 一个小提示:在“构造函数”和“析构函数”函数的名称中包含“类”这个词会令人困惑,并且可能表明您对这些东西保持了某种错误的心理模型:此类函数初始化/取消初始化一个类型的 instances(好吧,Go 没有类,但类 C++ PL 中的类是类型),即具有特定类型的具体变量。他们不会初始化/取消初始化类型本身。基本上这就是为什么“构造函数”函数在 Go 中通常被称为 New()NewWhatever():它们给你一个新的初始化值。
  • 您可能会发现this 对在编写良好的 Go 代码中命名包、类型和函数特别感兴趣。请务必同时查看标题为“进一步阅读”的部分。
  • @kostix initClass 和 deinitClass 的使用只是通用的,我只是使用名称来显示我在做什么。我创建了一个名为 initStructName 的结构来做一些花哨的事情,然后我添加了 defer deinitStructName、关闭文件和正确清理等。

标签: go


【解决方案1】:

Go 中有终结器。我写了一点blog post 关于它。它们甚至用于关闭标准库中的文件,如您所见here。 但是,我认为使用 defer 更可取,因为它更具可读性且不那么神奇。

【讨论】:

  • 如果你正在获取一些没有终结器的有限资源,并且如果它不断泄漏,程序最终将无法继续,你应该考虑设置终结器。同时,由于它们直到 GC 才运行,所以最好 defer 并立即释放资源,而不是依赖终结器。换句话说,终结器可能是一种有用的保险政策,可以防止其他难以堵塞的泄漏,但它们并不适合作为主要的资源管理机制。
  • @twotwotwo 请做出正确的回答。该评论可能不会被注意到,并且大声而明确地声明永远不应使用终结器,并且必须实施并记录用户明确表示“我已完成此对象”的方式。
  • 好电话@kostix,充实了这一点并将其添加为答案。
【解决方案2】:

runtime.SetFinalizer(ptr, finalizerFunc) 设置了一个终结器——不是析构函数,而是另一种可能最终释放资源的机制。阅读那里的文档以获取详细信息,包括缺点。它们可能要等到对象实际上无法访问很久之后才会运行,如果程序先退出,它们可能根本不会运行。他们还推迟释放内存以进行另一个 GC 周期。

如果您正在获取一些没有终结器的有限资源,并且如果它不断泄漏,程序最终将无法继续,您应该考虑设置终结器。它可以减轻泄漏。无法访问的文件和网络连接已经被 stdlib 中的终结器清理了,因此只有其他类型的资源可以使用自定义资源。最明显的一类是你通过syscallcgo获取的系统资源,但我可以想象其他的。

虽然它们可以修补一些错误,但您不应该依赖终结器。在 GC 运行之前,它们不会运行。因为程序可以在下一次 GC 之前退出,所以你不能依赖它们来完成必须完成的事情,比如将缓冲的输出刷新到文件系统。假设 GC 确实 发生,它可能不会很快发生:如果终结器负责关闭网络连接,则远程主机可能在 GC 之前达到其对您的打开连接的限制,或者您的进程命中其文件-descriptor 限制,或者你用完了ephemeral ports,或者别的什么。因此,defer 并在必要时立即进行清理 比使用终结器并希望它尽快完成要好得多。

在日常 Go 编程中您不会看到很多 SetFinalizer 调用,部分原因是最重要的调用位于标准库中,主要是因为它们的一般适用范围有限。

简而言之,终结器可以通过释放长期运行的程序中被遗忘的资源来提供帮助,但由于它们的行为并不能得到保证,因此它们不适合作为主要的资源管理机制。

【讨论】:

  • SetFinalizer 是唯一可用于释放 cgo 资源的方法,以符合他们的 golang 依赖项被垃圾收集的时间 - 它对于 c 包装器非常有用,特别是当 C 对象对何时可以被限制时相对于彼此释放。这应该是公认的答案。
  • 其实这是最有帮助的答案。我打算使用它来保护文件不被清理,同时它们仍然可以从代码中引用/访问(使用存储的文件名,而不是打开的文件)。而且,对这些文件(文件名)的引用具有不可预测的生命周期。
  • @kravemir 感谢您的赞赏!这可能不适用于您的情况或为时已晚无法使用,但需要注意的是,至少在 Linux 上,您可以保留一个指向已删除文件的打开文件句柄。然后在所有文件句柄都关闭(明确地或通过终结器或程序关闭)之后,操作系统实际上将释放磁盘上的空间。根据您正在执行的操作的详细信息,这可能允许您进行自动文件清理,而无需在 Go 端进行大量实施工作。
【解决方案3】:

在 Go 生态系统中,存在一种处理包装珍贵(和/或外部)资源的对象的普遍习惯:一种指定用于释放该资源的特殊方法,称为显式 - 通常通过defer 机制。

这个特殊的方法通常被命名为Close(),对象的用户在使用完对象所代表的资源后必须显式地调用它。 io 标准包甚至有一个特殊的接口io.Closer,声明了那个单一的方法。在 TCP 套接字、UDP 端点和文件等各种资源上实现 I/O 的对象都满足io.Closer,并且在使用后预计会显式为Closed。

调用此类清理方法通常是通过defer 机制完成的,该机制保证无论在资源获取后执行的某些代码是否会panic(),该方法都会运行。

您可能还注意到,在 Go 中没有隐式“析构函数”与没有隐式“构造函数”相当平衡。这实际上与 Go 中没有“类”无关:语言设计者只是尽可能地避免魔法


请注意,Go 解决此问题的方法可能看起来有点低技术,但实际上它是具有垃圾收集功能的运行时唯一可行的解​​决方案。在具有对象但没有 GC 的语言中,例如 C++,破坏对象是一个定义明确的操作,因为对象在超出范围或在其内存块上调用 delete 时被销毁。在带有 GC 的运行时中,对象将在未来某个不确定的点被 GC 扫描销毁,并且可能根本不会被销毁。因此,如果对象包装了一些宝贵的资源,那么该资源可能会在最后一个对封闭对象的实时引用丢失的那一刻被回收,甚至可能根本不会被回收——正如@twotwotwo 在他们各自的答案中很好地解释的那样。

另一个需要考虑的有趣方面是 Go 的 GC 是完全并发的(与常规程序执行一起)。这意味着即将收集死对象的 GC 线程可能(并且通常会)不是在该对象处于活动状态时执行该对象代码的线程。反过来,这意味着如果 Go 类型可以有析构函数,那么程序员需要确保析构函数执行的任何代码都与程序的其余部分正确同步——如果对象的状态影响到它外部的某些数据结构。这实际上可能会迫使程序员添加此类同步,即使对象的正常操作不需要它(并且大多数对象都属于此类)。想想那些在对象的析构函数被调用之前恰好被销毁的外部数据结构会发生什么(GC 以非确定性的方式收集死对象)。换句话说,当对象被显式编码到程序流中时,它更容易控制和推理对象销毁:既可以指定何时必须销毁对象,也可以保证其销毁的正确顺序。它外部的数据结构。

如果您熟悉 .NET,它处理资源清理的方式与 Go 非常相似:包装一些宝贵资源的对象必须实现 IDisposable 接口和一个方法 @987654335由该接口导出的 @ 必须在完成此类对象后显式调用。 C# 通过using 语句为这个用例提供了一些语法糖,这使得编译器安排在对象超出所述语句声明的范围时调用Dispose()。在 Go 中,您通常会 defer 调用清理方法。


还有一点要注意。 Go 希望您非常认真地对待错误(与大多数带有 "just throw an exception and don't give a fsck about what happens due to it elsewhere and what state the program will be in" attitude 的主流编程语言不同),因此您可以考虑检查至少 一些 对清理方法的调用的错误返回。

一个很好的例子是os.File 类型的实例表示文件系统上的文件。有趣的是,在打开的文件上调用Close()可能会失败,如果您正在写入该文件,这可能表明并非所有数据你写的那个文件实际上是在文件系统上登陆的。有关解释,请阅读close(2) manual中的“注释”部分。

换句话说,只是做类似的事情

fd, err := os.Open("foo.txt")
defer fd.Close()

在 99.9% 的情况下对于只读文件是可以的,但对于打开写入的文件,您可能希望实施更多涉及的错误检查和一些处理它们的策略(仅报告、等待然后重试、问-然后-也许-重试或其他)。

【讨论】:

  • 很好的答案,用于解释您应该做什么。 ZeroMQ 链接也很酷——个人发现,当您需要完全了解每个错误路径下发生的情况时,错误返回非常有用。
  • 指向 ZMQ 作者比较 C 和 C++ 的链接非常可怕。我没有看到任何实际的论点(注意:我不排除在 ZMQ 中有些地方使用异常是一个糟糕的选择!)支持这些声明。我宁愿说作者造了几个稻草人,然后把它们拆掉。例如,说 C++ 是面向对象的。例如,忽略移动语义。 @kostix,你的回答很好,但没有这个链接会更好。
  • @UlrichEckhardt,不幸的是,我知道没有其他可用材料可以像那个那样解释异常问题,所以我倾向于让它完全保留那个 i> 原因。您的评论可能会很好地作为附加到该链接的一种“警告购买者”;-)
  • @eli-bendersky 个人网站上的一些文章接近但不完全。比如我喜欢this one
猜你喜欢
  • 2023-03-20
  • 2012-04-07
  • 2017-06-13
  • 2012-03-30
  • 1970-01-01
  • 1970-01-01
  • 2012-08-30
  • 2011-05-24
  • 2018-07-14
相关资源
最近更新 更多