Aug 26, 2015
Finalizers
Finalizer is basically a function which will be called when your object will lost
all references and will be found by GC. In Go you can add finalizers to your
objects with runtime.SetFinalizer
function. Let’s see how it works.
package main
import (
"fmt"
"runtime"
"time"
)
type Test struct {
A int
}
func test() {
// create pointer
a := &Test{}
// add finalizer which just prints
runtime.SetFinalizer(a, func(a *Test) { fmt.Println("I AM DEAD") })
}
func main() {
test()
// run garbage collection
runtime.GC()
// sleep to switch to finalizer goroutine
time.Sleep(1 * time.Millisecond)
}
I AM DEAD
So, we created object a
which is pointer and set simple finalizer to it. When
code left test function - all references to it disappeared and therefore garbage
collector was able to collect a
and call finalizer in its own goroutine. You
can try to modify test()
function to return *Test
an print it in main()
,
then you’ll see that finalizer won’t be called.
Also if you remove A
field from Test
type, because then Test
became just
empty struct and empty struct allocates no memory and can’t be collected by GC.
Finalizers examples
Let’s try to find finalizers usage in standard library. There it is used only for
for closing file descriptors like this in net
package:
runtime.SetFinalizer(fd, (*netFD).Close)
Close
net.Conn
.
So probably finalizers not so good idea if even in standard library it has so limited usage. Let’s see what problems can be.
Why you should avoid finalizers
Finalizers is pretty tempting idea if you come from languages without GC or where
you’re not expecting users to write proper code. In Go we have both GC and
pro-users :) So, in my opinion explicit call of Close
is always better than
magic finalizer. For example there is finalizer for fd in os
:
// NewFile returns a new File with the given file descriptor and name.
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
}
NewFile
is called by OpenFile
which is called by Open
, so if you’re
opening file you’ll hit that code. Problem with finalizers that you have no
control over them, and more than that you’re not expecting them. Look at code:
func getFd(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return -1, err
}
return f.Fd(), nil
}
getFd
f
loses its last reference and so your file is doomed to
be closed sooner or later (when next GC cycle will come). Here is problem not
that file will be closed, but that it’s not documented and not expected at all.
Conclusion
I think it’s better to suppose that users are smart enough to cleanup object
themselves. At least all methods which call SetFinalizer
should document this,
but I personally don’t see any value in this method for me.