英文:
Go destructors?
问题
我知道Go语言中没有析构函数,因为从技术上讲,它没有类的概念。因此,我使用initClass
来执行与构造函数相同的功能。然而,是否有任何方法可以在终止时创建类似析构函数的东西,用于关闭文件等操作?目前,我只是调用defer deinitClass
,但这种方法有些笨拙,我认为这是一个不良的设计。有什么更好的方法吗?
英文:
I know there are no destructors in Go since technically there are no classes. As such, I use initClass
to perform the same functions as a constructor. However, is there any way to create something to mimic a destructor in the event of a termination, for the use of, say, closing files? Right now I just call defer deinitClass
, but this is rather hackish and I think a poor design. What would be the proper way?
答案1
得分: 67
在Go生态系统中,存在一种处理包装宝贵(和/或外部)资源的对象的普遍惯用语法:为释放该资源指定一个特殊的方法,称为显式,通常通过defer
机制来实现。
这个特殊的方法通常被命名为Close()
,当用户完成对对象所代表的资源的使用时,必须显式调用它。甚至io
标准包都有一个特殊的接口io.Closer
,声明了这个单一的方法。实现对各种资源(如TCP套接字、UDP端点和文件)进行I/O的对象都满足io.Closer
接口,并且在使用后需要显式地进行Close
操作。
调用这样的清理方法通常通过defer
机制来实现,该机制保证无论在资源获取后执行的某些代码是否引发panic()
,该方法都将运行。
你可能还注意到,在Go中没有隐式的“析构函数”与没有隐式的“构造函数”相当。这实际上与Go中没有“类”无关:语言设计者只是尽可能地避免使用魔法。
请注意,Go对这个问题的处理方式可能看起来有些低技术含量,但实际上它是唯一可行的解决方案,因为它运行时具有垃圾回收功能。在一个有对象但没有垃圾回收的语言中,比如C++,销毁对象是一个明确定义的操作,因为对象要么在超出作用域时被销毁,要么在其内存块上调用delete
时被销毁。在具有垃圾回收的运行时中,对象将在未来的某个不确定的时间点被垃圾回收扫描销毁,并且可能根本不会被销毁。因此,如果对象包装了一些宝贵的资源,那么该资源可能会在最后一个对封闭对象的活动引用丢失的时间点之后被回收,甚至可能根本不会被回收,正如@twotwotwo在他们的回答中所解释的那样。
另一个有趣的方面是,Go的垃圾回收是完全并发的(与常规程序执行并发)。这意味着即将收集死对象的GC线程可能(通常)不是执行该对象代码时的线程。反过来,这意味着如果Go类型可以有析构函数,那么程序员需要确保析构函数执行的任何代码与程序的其余部分正确同步,如果对象的状态影响到其外部的某些数据结构。这实际上可能会迫使程序员添加这样的同步,即使对象的正常操作不需要它(大多数对象都属于这种类别)。想象一下,如果这些外部数据结构在对象的析构函数被调用之前被销毁会发生什么(GC以非确定性的方式收集死对象)。换句话说,当对象的销毁被显式编码到程序的流程中时,更容易控制对象的销毁,并且更容易推理关于对象销毁的顺序与其外部数据结构销毁的顺序的问题。
如果你熟悉.NET,你会发现它在处理资源清理方面与Go非常相似:包装某些宝贵资源的对象必须实现IDisposable
接口,并且该接口导出了一个名为Dispose()
的方法,当你完成对这样的对象的使用时,必须显式调用它。C#通过using
语句为这种用例提供了一些语法糖,当对象超出该语句声明的作用域时,编译器会安排调用Dispose()
。在Go中,你通常会使用defer
调用清理方法。
还有一个需要注意的地方。Go希望你非常认真地对待错误(与大多数主流编程语言的“只是抛出异常,不关心它在其他地方引发了什么问题以及程序处于什么状态”的态度不同),所以你可能要考虑检查至少一些清理方法的错误返回。
一个很好的例子是表示文件系统上的文件的os.File
类型的实例。有趣的是,调用打开文件的Close()
方法可能由于合法的原因而失败,如果你正在对该文件进行写入,这可能表明你写入文件的数据并没有真正地落地到文件系统中。有关说明,请阅读close(2)
手册中的“Notes”部分。
换句话说,仅仅像下面这样做:
fd, err := os.Open("foo.txt")
defer fd.Close()
对于只读文件来说,在99.9%的情况下是可以的,但对于打开用于写入的文件,你可能需要实现更复杂的错误检查和处理策略(仅报告、等待然后重试、询问然后可能重试或其他)。
英文:
In the Go ecosystem, there exists a ubiquitous idiom for dealing with objects which wrap precious (and/or external) resources: a special method designated for freeing that resource, called explicitly — typically via the defer
mechanism.
This special method is typically named Close()
, and the user of the object has to call it explicitly when they're done with the resource the object represents. The io
standard package does even have a special interface, io.Closer
, declaring that single method. Objects implementing I/O on various resources such as TCP sockets, UDP endpoints and files all satisfy io.Closer
, and are expected to be explicitly Close
d after use.
Calling such a cleanup method is typically done via the defer
mechanism which guarantees the method will run no matter if some code which executes after resource acquisition will panic()
or not.
You might also notice that not having implicit "destructors" quite balances not having implicit "constructors" in Go. This actually has nothing to do with not having "classes" in Go: the language designers just avoid magic as much as practically possible.
Note that Go's approach to this problem might appear to be somewhat low-tech but in fact it's the only workable solution for the runtime featuring garbage-collection. In a language with objects but without GC, say C++, destructing an object is a well-defined operation because an object is destroyed either when it goes out of scope or when delete
is called on its memory block. In a runtime with GC, the object will be destroyed at some mostly indeterminate point in the future by the GC scan, and may not be destroyed at all. So if the object wraps some precious resource, that resource might get reclaimed way past the moment in time the last live reference to the enclosing object was lost, and it might even not get reclaimed at all—as has been well explained by @twotwotwo in their respective answer.
Another interesting aspect to consider is that the Go's GC is fully concurrent (with the regular program execution). This means a GC thread which is about to collect a dead object might (and usually will) be not the thread(s) which executed that object's code when it was alive. In turn, this means that if the Go types could have destructors then the programmer would need to make sure whatever code the destructor executes is properly synchronized with the rest of the program—if the object's state affects some data structures external to it. This actually might force the programmer to add such synchronization even if the object does not need it for its normal operation (and most objects fall into such category). And think about what happens of those exernal data strucrures happened to be destroyed before the object's destructor was called (the GC collects dead objects in a non-deterministic way). In other words, it's much easier to control — and to reason about — object destruction when it is explicitly coded into the program's flow: both for specifying when the object has to be destroyed, and for guaranteeing proper ordering of its destruction with regard to destroying of the data structures external to it.
If you're familiar with .NET, it deals with resource cleanup in a way which resembles that of Go quite closely: your objects which wrap some precious resource have to implement the IDisposable
interface, and a method, Dispose()
, exported by that interface, must be called explicitly when you're done with such an object. C# provides some syntactic sugar for this use case via the using
statement which makes the compiler arrange for calling Dispose()
on the object when it goes out of the scope declared by the said statement. In Go, you'll typically defer
calls to cleanup methods.
One more note of caution. Go wants you to treat errors very seriously (unlike most mainstream programming language with their "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) and so you might consider checking error returns of at least some calls to cleanup methods.
A good example is instances of the os.File
type representing files on a filesystem. The fun stuff is that calling Close()
on an open file might fail due to legitimate reasons, and if you were writing to that file this might indicate that not all the data you wrote to that file had actually landed in it on the file system. For an explanation, please read the "Notes" section in the close(2)
manual.
In other words, just doing something like
fd, err := os.Open("foo.txt")
defer fd.Close()
is okay for read-only files in the 99.9% of cases, but for files opening for writing, you might want to implement more involved error checking and some strategy for dealing with them (mere reporting, wait-then-retry, ask-then-maybe-retry or whatever).
答案2
得分: 27
runtime.SetFinalizer(ptr, finalizerFunc)
设置一个 finalizer(不是析构函数,而是另一种可能最终释放资源的机制)。请阅读文档了解详细信息,包括其中的缺点。finalizer可能在对象实际上不可达之后很久才运行,并且如果程序先退出,则可能根本不会运行。它们还会推迟释放内存,等待下一次垃圾回收循环。
如果你正在获取一些有限的资源,而该资源尚未具有 finalizer,并且如果资源泄漏,程序最终将无法继续运行,那么你应该考虑设置一个 finalizer。它可以减轻资源泄漏的问题。在标准库中,无法访问的文件和网络连接已经通过 finalizer 进行清理,因此只有其他类型的资源才需要自定义 finalizer。最明显的类别是通过 syscall
或 cgo
获取的系统资源,但我可以想象还有其他情况。
finalizer 可以帮助最终释放资源,即使使用该资源的代码省略了 Close()
或类似的清理操作,但它们的行为太不可预测,不能成为主要的资源管理机制。它们只有在进行垃圾回收时才会运行。由于程序可能在下一次垃圾回收之前退出,因此你不能依赖它们来执行必须完成的任务,例如将缓冲输出刷新到文件系统。如果确实发生了垃圾回收,它可能发生得不够及时:如果一个 finalizer 负责关闭网络连接,可能在垃圾回收之前,远程主机达到了对你的开放连接数限制,或者你的进程达到了文件描述符限制,或者你用尽了临时端口,或者其他情况。因此,最好的做法是在必要时使用 defer
进行清理,而不是使用 finalizer 并希望它能够及时完成。
在日常的 Go 编程中,很少见到 SetFinalizer
的调用,部分原因是最重要的 finalizer 已经在标准库中,主要是因为它们的适用范围有限。
简而言之,finalizer 可以通过释放长时间运行的程序中被遗忘的资源来帮助,但由于它们的行为没有太多保证,它们不适合作为主要的资源管理机制。
英文:
runtime.SetFinalizer(ptr, finalizerFunc)
sets a finalizer--not a destructor but another mechanism to maybe eventually free up resources. Read the documentation there for details, including downsides. They might not run until long after the object is actually unreachable, and they might not run at all if the program exits first. They also postpone freeing memory for another GC cycle.
If you're acquiring some limited resource that doesn't already have a finalizer, and the program would eventually be unable to continue if it kept leaking, you should consider setting a finalizer. It can mitigate leaks. Unreachable files and network connections are already cleaned up by finalizers in the stdlib, so it's only other sorts of resources where custom ones can be useful. The most obvious class is system resources you acquire through syscall
or cgo
, but I can imagine others.
Finalizers can help get a resource freed eventually even if the code using it omits a Close()
or similar cleanup, but they're too unpredictable to be the main way to free resources. They don't run until GC does. Because the program could exit before next GC, you can't rely on them for things that must be done, like flushing buffered output to the filesystem. If GC does happen, it might not happen soon enough: if a finalizer is responsible for closing network connections, maybe a remote host hits its limit on open connections to you before GC, or your process hits its file-descriptor limit, or you run out of ephemeral ports, or something else. So it's much better to defer
and do cleanup right when it's necessary than to use a finalizer and hope it's done soon enough.
You don't see many SetFinalizer
calls in everyday Go programming, partly because the most important ones are in the standard library and mostly because of their limited range of applicability in general.
In short, finalizers can help by freeing forgotten resources in long-running programs, but because not much about their behavior is guaranteed, they aren't fit to be your main resource-management mechanism.
答案3
得分: 9
在Go语言中有Finalizers。我写了一篇关于它的小博客文章,你可以在这里看到。甚至在标准库中,它们也被用于关闭文件,你可以在这里看到。
然而,我认为使用defer更可取,因为它更易读,而且不那么神奇。
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论