📜 ⬆️ ⬇️

The mystery of the finalizers in Go

Finalizers


When the Go garbage collector is ready to collect an object left without references, a function called a finalizer is called first. You can add such a function to your object using runtime.SetFinalizer . Let's look at it in work:

 package main import (   "fmt"   "runtime"   "time" ) type Test struct {   A int } func test() {   //     a := &Test{}   //      runtime.SetFinalizer(a, func(a *Test) { fmt.Println("I AM DEAD") }) } func main() {   test()   //      runtime.GC()   //        time.Sleep(1 * time.Millisecond) } 

Obviously, the output will be:
I AM DEAD

So, we created an object a , which is a pointer, and set a simple finalizer on it. When the test() function completes, all references to a disappear, and the garbage collector is given permission to assemble it and, therefore, call the finalizer in its own gorutin. Try changing the test() so that it returns *Test and prints it in main () - you will find that the finalizer was not called. The same thing happens if you remove the A field from the Test type - the structure will be empty, and the empty structures do not take up memory and do not require cleaning by the garbage collector.

Finalizer examples


The source code for the standard Go library is great for learning a language. Let's try to find examples of finalizers in it - and find only their use when closing file descriptors, such as in the net package:
 runtime.SetFinalizer(fd, (*netFD).Close) 

Thus, the file descriptor will never leak, even if you forget to call Close on net.Conn .
Maybe finalizers are not such a cool thing, since the authors of the standard library almost never used them? Let's see what problems may be with them.

Why finalizers should be avoided


The idea of ​​using finalizers is quite attractive, especially for adherents of languages ​​without GC or in those cases where you do not expect high-quality code from users. In Go, we have both GC and experienced developers, so in my opinion, it’s always better to explicitly call Close rather than use the magic of finalizers. For example, here is the os finalizer that handles the file descriptor:
 func NewFile(fd uintptr, name string) *File {   fdi := int(fd)   if fdi < 0 {       return nil   }   f := &File{&file{fd: fdi, name: name}}   runtime.SetFinalizer(f.file, (*file).close)   return f } 

os.NewFile is called by the os.OpenFile function, which in turn is called from os.Open , so this code is executed each time the file is opened. One of the problems of the finalizers is that they are beyond our control, but, worse, they are unexpected. Take a look at the code:
 func getFd(path string) (int, error) {   f, err := os.Open(path)   if err != nil {       return -1, err   }   return f.Fd(), nil } 

This is a common approach to getting a file descriptor in a specific path when developing on Linux. But this code is unreliable: when returning from getFd the f object loses the last link, and your file is doomed to close soon (at the next garbage collection cycle). But the problem here is not that the file is closed, but that this behavior is undocumented and completely unexpected.
')

Conclusion


I think it is better to consider users moderately intelligent and able to clean up objects themselves. At least, all methods that call SetFinalizer (even not directly, as in the example with os.Open ), should have a corresponding mention in the documentation. I personally find this method useless and may even be a bit harmful.

EDIT 1: ivan4th gave an example where the use of finalizers is appropriate (clearing the memory in C code): link
EDIT 2: JIghtuse rightly pointed out that the behavior of the Fd method is now documented: link . That once again confirms that it would be nice to document your finalizers too.

Source: https://habr.com/ru/post/268841/


All Articles