Go: Finalizers

Vincent Blanchon
Jul 20 · 4 min read
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.12.

Go runtime provides a method runtime.SetFinalizer that allows developers to attach a function to a variable that will be called when the garbage collector sees this variable as garbage ready to be collected since it became unreachable. This feature is highly subject to debate and this article does not aim to participate in it, but rather to explain the implementation of the method.

No guarantees

Let’s take an example of a program that uses finalizers:

This program will create, in a loop, three instances of a struct and attach a finalizer to it. Then, the garbage collector will be called to collect the instances created previously. Running this program will give us the following output:

31
37
47

As we can see, finalizers have not been called. The documentation of the runtime could explain this point:

The finalizer is scheduled to run at some arbitrary time after the program can no longer reach the object to which obj points. There is no guarantee that finalizers will run before a program exits, so typically they are useful only for releasing non-memory resources associated with an object during a long-running program

Runtime does not provide any guarantee about the delay before the finalizer will be called. Let’s try to modify our program by adding a one second sleep after calling the garbage collector:

31
37
47
foo 1 has been garbage collected
foo 0 has been garbage collected

Now our finalizers have been called. However, one is missing. Our finalizers are tied to the Go garbage collector and the way it will collect and clean the data will impact the call to the finalizers.

Workflow

The previous example could let us think that Go calls our finalizers just before freeing the memory made by our structs.

Let’s see exactly what happens with more allocations:

One million structs with finalizers are created, here is the output:

Allocation: 0.090862 Mb, Number of allocation: 137
Allocation: 31.107506 Mb, Number of allocation: 2390078
Allocation: 110.052666 Mb, Number of allocation: 4472742

Let’s try again without finalizers:

Allocation: 0.090694 Mb, Number of allocation: 136
Allocation: 18.129814 Mb, Number of allocation: 1390078
Allocation: 0.094451 Mb, Number of allocation: 154

It seems that nothing has been cleaned in memory despite the garbage collector being triggered and the finalizers run. Let’s go back to the runtime documentation in order to understand this behavior:

When the garbage collector finds an unreachable block with an associated finalizer, it clears the association and runs finalizer(obj) in a separate goroutine. This makes obj reachable again, but now without an associated finalizer. Assuming that SetFinalizer is not called again, the next time the garbage collector sees that obj is unreachable, it will free obj.

As we can see, the finalizers are first removed, and the memory will be released on the next cycle. Let’s run our first example again with two forced garbage collections:

Allocation: 0.090862 Mb, Number of allocation: 137
Allocation: 31.107506 Mb, Number of allocation: 2390078
Allocation: 110.052666 Mb, Number of allocation: 4472742
Allocation: 0.099220 Mb, Number of allocation: 166

We can clearly see that the second run will actually clean the data. The finalizers could therefore slightly affect the performance and memory usage.

Performance

The finalizers run one after another as explained in the documentation:

A single goroutine runs all finalizers for a program, sequentially. If a finalizer must run for a long time, it should do so by starting a new goroutine.

Only one goroutine will run the finalizers and any heavy task should create a new goroutine. When the finalizers run, the garbage collector does not stop the world and run in concurrency; it should therefore not affect your application performance.

Also, Go provides a way to remove a finalizer if it is not needed anymore:

runtime.SetFinalizer(p, nil)

It allows us to dynamically remove them depending on our usage.

Application usage

Internally, Go uses the finalizers in the net, net/http packages to ensures the files previously opened are closed, and in the os package to ensure the created processes are released well. Here is an example from the os package:

func newProcess(pid int, handle uintptr) *Process {
p := &Process{Pid: pid, handle: handle}
runtime.SetFinalizer(p, (*Process).Release)
return p
}

When the process is actually released, the finalizer will be removed:

func (p *Process) release() error {
// NOOP for unix.
p.Pid = -1
// no need for a finalizer anymore
runtime.SetFinalizer(p, nil)
return nil
}

Go also uses finalizers in the tests to ensure specific expected actions are done at the garbage collection. For example, the sync package uses the finalizers to test if the pool is cleaned during the garbage collection cycle.

A Journey With Go

Vincent Blanchon

Written by

French Gopher in Dubai

A Journey With Go

A Journey With Go Language Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade