Go 析构函数?

44

我知道Go语言中没有析构函数,因为从技术上讲,它没有类。因此,我使用initClass来执行与构造函数相同的功能。但是,在终止时是否有任何方法可以创建类似于析构函数的东西,以便用于关闭文件等用途?目前,我只是调用defer deinitClass,但这样做有点欺骗性,我认为这是一个不良的设计。正确的方式是什么?


2
一个小提醒:在“构造函数”和“析构函数”的名称中使用“class”这个词会让人感到困惑,并且可能表明您对此类事物的心理模型存在一些错误:这些函数初始化/去初始化类型的实例(好吧,Go没有类,但是C++-like PLs中的类是类型),也就是具有特定类型的具体变量。它们不会初始化/去初始化类型本身。基本上这就是为什么在Go中,“构造函数”通常被称为New()NewWhatever():它们给你一个新的初始化 - kostix
2
您可能会对如何在编写良好的Go代码中命名包、类型和函数感兴趣,可以参考这篇文章。同时请务必查看其中标题为“Further reading”的部分。 - kostix
@kostix,initClass和deinitClass的使用只是通用的,我只是使用这些名称来展示我正在做什么。我创建了一个结构体,然后调用initStructName来执行一些花哨的操作,然后在其下面添加了defer deinitStructName,以关闭文件并进行适当的清理等工作。 - kdgwill
3个回答

68
在 Go 生态系统中,存在一种普遍的习语来处理包装宝贵(和/或外部)资源的对象:专门为释放该资源指定的特殊方法,称为显式方法 - 通常通过 defer 机制实现。
这个特殊的方法通常被命名为 Close(),对象的用户必须在使用完对象所代表的资源后显式调用它。甚至 io 标准包都有一个特殊的接口,io.Closer,声明了这个单一的方法。实现各种资源上的 I/O,如 TCP sockets、UDP 端点和文件的对象都满足 io.Closer,并且在使用后应该显式地进行 Close
调用这样的清理方法通常是通过 defer 机制完成的,该机制保证无论在获取资源后执行的某些代码是否会 panic(),方法都将运行。
你可能还注意到,在Go语言中没有隐式“析构函数”,这与没有隐式“构造函数”相当平衡。这实际上与Go语言中没有“类”无关:语言设计者只是尽可能避免使用“魔法”。
请注意,Go语言对这个问题的解决方案可能看起来有些低级,但实际上它是运行时垃圾回收唯一可行的解决方案。在一个具有对象但没有GC的语言(比如C++)中,析构对象是一个明确定义的操作,因为对象要么在超出作用域时被销毁,要么在其内存块上调用delete时被销毁。在具有GC的运行时中,对象将在未来的某个不确定时间点被GC扫描销毁,并且可能根本不会被销毁。因此,如果对象包装了一些珍贵的资源,那么该资源可能会在最后一个活动引用到封闭对象的时刻过去很久才被回收,甚至可能根本不会被回收,正如@twotwotwo在他们各自的答案中所解释的那样。
另一个有趣的方面是Go的垃圾回收器是完全并发的(与常规程序执行一起)。这意味着即将收集死对象的GC线程可能(通常会)不是执行该对象代码时存活的线程。反过来,这意味着如果Go类型可以具有析构函数,则程序员需要确保析构函数执行的任何代码与程序的其余部分正确同步,如果对象状态影响某些外部数据结构。这实际上可能会迫使程序员添加这种同步,即使对象对其正常操作不需要它(大多数对象都属于这种类别)。想象一下,如果这些外部数据结构在对象的析构函数被调用之前被销毁会发生什么(GC以非确定性方式收集死对象)。换句话说,当对象的销毁明确编码到程序流中时,更容易控制并推理对象的销毁:既可以指定对象何时必须被销毁,也可以保证其销毁顺序与销毁外部数据结构的顺序正确。
如果你熟悉.NET,那么它处理资源清理的方式与Go非常相似:包装某些宝贵资源的对象必须实现IDisposable接口,并且该接口导出了一个名为Dispose()的方法,在完成对象使用后必须显式调用。C#通过using语句为此提供了一些语法糖,当对象超出所述语句声明的范围时,编译器会安排调用Dispose()。在Go中,通常会延迟调用清理方法。

还有一点需要注意的是,Go语言要求您非常严肃地对待错误(与大多数主流编程语言不同,它们采用“只需抛出异常,不关心其他地方会发生什么以及程序将处于什么状态”的态度),因此您可能需要考虑检查至少一些清理方法的错误返回。

一个很好的例子是代表文件系统上的文件的os.File类型的实例。 有趣的是,调用打开文件的Close()可能会由于合法原因而失败,如果您正在向该文件写入,这可能表明您写入到该文件的并非全部真正落在了文件系统中。 有关说明,请阅读close(2)手册中的“注释”部分。

换句话说,仅仅做类似以下操作是不够的:

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

对于只读文件,在99.9%的情况下使用是可以的,但对于打开写入权限的文件,您可能需要实现更复杂的错误检查和一些处理策略(仅报告、等待再重试、询问再或许重试或其他)。


4
很好的回答,解释了你应该做什么。那个 ZeroMQ 的链接也很棒——个人认为当你需要完全理解每个错误路径发生了什么时,错误返回值非常有用。 - twotwotwo
那个链接比较C和C++的ZMQ作者真的很糟糕。我没有看到任何实际的论据(注意:我不排除在ZMQ中使用异常是一个糟糕的选择的情况!)支持这些说法。我宁愿说作者建立了一些稻草人,然后将它们打倒。例如,说C++是面向对象的。例如,忽略移动语义。@kostix,你的回答很好,但如果没有这个链接会更好。 - Ulrich Eckhardt
@UlrichEckhardt,不幸的是,我不知道还有其他可用的材料能像那个一样详细阐述异常问题,所以我倾向于让它保留,正是因为这个原因。你的评论可以作为一个“买家注意”附在那个链接上;-) - kostix
@eli-bendersky的个人网站上有一些相似的文章,但并不完全符合我的需求。比如我喜欢这篇文章 - kostix

27

runtime.SetFinalizer(ptr, finalizerFunc)设置一个finalizer--不是析构函数,而是另一种可能最终释放资源的机制。详见文档,包括其缺点。它们可能在对象实际上已经无法访问之后很长时间才运行,并且如果程序先退出,则它们可能根本不会运行。它们还会延迟到下一个垃圾回收周期来释放内存。

如果你正在获取一些有限的资源,该资源尚未具备finalizer,而且如果它持续泄漏,程序最终将无法继续运行,那么应该考虑设置finalizer。它可以减少泄漏。在stdlib中,不可访问的文件和网络连接已由finalizer清理,因此只有其他类型的资源才需要自定义finalizer。其中最明显的类是通过syscallcgo获取的系统资源,但我也可以想象其他类型的资源。

Finalizer可以帮助最终释放资源,即使使用它的代码省略了Close()或类似的清理操作,但它们太不可预测了,不能成为释放资源的主要方式。它们只有在GC运行时才会运行。因为程序可能在下一个GC之前退出,所以你不能依赖它们来完成必须要做的事情,比如将缓冲输出刷新到文件系统中。如果GC确实发生了,它可能不会及时发生:如果finalizer负责关闭网络连接,则可能在GC之前远程主机已达到其打开到您的连接的限制,或者您的进程达到了其文件描述符限制,或者您用尽了瞬态端口,或者其他某些原因。因此,与使用finalizer并希望它能及时完成处理相比,更好的方法是使用defer和在必要时立即进行清理。

在日常的Go编程中,你很少看到SetFinalizer调用,部分原因是其中最重要的调用已经在标准库中,大多数原因是它们适用范围有限。

简而言之,终结器可以在长时间运行的程序中释放被遗忘的资源,但由于它们的行为没有太多保证,它们不适合作为您主要的资源管理机制。


2
SetFinalizer是唯一可用的方法,可以在其golang依赖项被垃圾回收时释放cgo资源 - 对于c包装器非常有用,特别是当C对象相对于彼此的释放时间存在限制时。这应该是被接受的答案。 - domoarigato
1
实际上,这是最有帮助的答案。我计划使用它来保护文件不被清理,同时仍然可以通过代码(使用存储的文件名,而不是已打开的文件)访问/引用这些文件。而且,对这些文件(文件名)的引用具有不可预测的生命周期。 - kravemir
@kravemir 感谢您的赞赏!这可能与您的情况无关或太晚了,但是想指出至少在Linux上,您可以保持一个指向已删除文件的打开文件句柄。然后,在关闭所有文件句柄(显式地或通过终结器或程序关闭)之后,操作系统将实际释放磁盘上的空间。根据您正在做的具体细节,这可能允许您在Go端不需要太多实现工作的情况下进行自动文件清理。 - twotwotwo

9
在Go语言中有Finalizers。我写了一篇关于它们的小博客文章,链接在这里。你甚至可以在标准库中看到它们被用来关闭文件,链接在这里。然而,我认为使用defer更好,因为它更易读且不那么神秘。

5
如果你正在获取一些有限的资源并且该资源尚未拥有终结器,而且如果资源继续泄漏程序最终将无法继续运行,那么你应该考虑设置一个终结器。同时,由于终结器只有在垃圾回收时才会运行,所以最好使用defer立即释放资源,而不是依赖于终结器。换句话说,终结器可能是一个有用的保险机制,可以防止难以弥补的泄漏,但它们不适合作为主要的资源管理机制。 - twotwotwo
2
@twotwotwo请将其转化为一个合适的答案。评论可能不会被注意到,因此应该明确表示终结器永远不应该被使用,并且必须实现和记录用户明确信号“我已经完成了这个对象”。 - kostix
1
好的,@kostix 的建议很好,我进行了详细说明并将其作为答案添加了进去。 - twotwotwo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接