Why Go’s AddCleanup Outperforms SetFinalizer for Safe Resource Cleanup
This article explains the limitations of Go's runtime.SetFinalizer, introduces the new runtime.AddCleanup function in Go 1.24, demonstrates proper usage with code examples, and highlights important pitfalls and best‑practice tips for reliable resource management.
Previously I wrote an article about runtime.SetFinalizer, a function that runs when an object is reclaimed, but it has several drawbacks that limit its usage.
When using SetFinalizer in Go, the object reference must point to the start of the allocated memory block, a concept not exposed at the language level. An object can have only one SetFinalizer . If an object with SetFinalizer participates in any reference cycle, it will never be released and the finalizer will not run. Objects with SetFinalizer require at least two GC cycles before they can be reclaimed (see https://github.com/golang/go/issues/67535).
For the first point, consider this example:
package main
import (
"fmt"
"runtime"
)
type MyStruct struct {
Field int
}
func main() {
obj := &MyStruct{Field: 42}
runtime.SetFinalizer(obj, func(m *MyStruct) {
fmt.Println("Finalizer called for:", m)
})
// Incorrect example: passing a field reference instead of the object itself
// runtime.SetFinalizer(&obj.Field, func(m *int) {
// fmt.Println("Finalizer called for:", *m)
// })
}Because of these issues, Go 1.24 introduced a new function runtime.AddCleanup to replace runtime.SetFinalizer.
Note: Neither runtime.AddCleanup nor runtime.SetFinalizer guarantees that the cleanup function will definitely run.
The design goal of AddCleanup is to solve the problems of SetFinalizer, especially avoiding object resurrection, allowing timely cleanup, and supporting cyclic cleanup.
The prototype of AddCleanup is:
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) CleanupUsing this, you can write an RAII‑style demo. In the "go weak" article we implemented a fixed‑size cache; by adding a newElemWeak method we can automatically call delete to remove a key from the cache when the oldest element is evicted:
func (c *WeakCache) newElemWeak(elem *list.Element) weak.Pointer[list.Element] {
elemWeak := weak.Make(elem)
runtime.AddCleanup(elem, func(name string) {
delete(c.cache, name)
}, elem.Value.(*CacheItem).key)
return elemWeak
}Things to Watch Out For
AddCleanupplaces few constraints on ptr and allows attaching multiple cleanup functions to the same pointer. However, if ptr becomes reachable from the cleanup or arg, it will never be reclaimed, causing a memory leak; the cleanup will never run, though this does not currently panic. Future detection might use GODEBUG=gccheckmark=1.
Example with a file resource:
func NewFileResource(filename string) (*os.File, error) {
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
runtime.AddCleanup(file, func(f *os.File) {
fmt.Println("close f")
_ = f.Close()
}, file)
return file, nil
}The fmt.Println("close f") line will not be printed and the file will not be closed if the cleanup never runs.
When multiple cleanups are bound to a pointer, they run in separate goroutines, so their execution order is nondeterministic.
Especially, if several objects reference each other and become unreachable simultaneously, their cleanup functions can all run in any order, even if the objects form a cycle (which would cause a memory leak with SetFinalizer).
Example demonstrating cyclic objects:
func main() {
x := MyStruct{Name: "X"}
y := MyStruct{Name: "Y"}
x.Other = &y
y.Other = &x
xName := x.Name
runtime.AddCleanup(&x, func(name string) {
fmt.Println("Cleanup for", x)
}, xName)
yName := y.Name
runtime.AddCleanup(&y, func(name string) {
fmt.Println("Cleanup for", y)
}, yName)
time.Sleep(time.Millisecond)
runtime.GC()
time.Sleep(time.Millisecond)
runtime.GC()
}Running the program produces:
Cleanup for Y
Cleanup for XThus, SetFinalizer can block GC, whereas AddCleanup executes as expected.
References
https://github.com/golang/go/issues/67535
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
