Go

What is runtime.AddCleanup and why is it preferred over SetFinalizer?

Question 355HardGo 1.22 to 1.25

Go 1.24 added:

func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
func (c Cleanup) Stop()

Once ptr is unreachable, the runtime calls cleanup(arg) at some point. The key difference is that the cleanup receives arg, not the object, so the object is never resurrected.

type File struct {
    fd      int
    cleanup runtime.Cleanup
}

func Open(path string) (*File, error) {
    fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
    if err != nil {
        return nil, err
    }
    f := &File{fd: fd}
    // Pass fd by value: the closure must not capture f.
    f.cleanup = runtime.AddCleanup(f, func(fd int) { syscall.Close(fd) }, fd)
    return f, nil
}

func (f *File) Close() error {
    f.cleanup.Stop() // explicit close: cancel the safety net
    return syscall.Close(f.fd)
}

Advantages over finalizers:

  • Memory can be freed in one cycle, because nothing is resurrected.
  • Cycles are fine: objects with cleanups in a cycle are still collected.
  • Several cleanups can be attached to one object, including through interior pointers.
  • Cancel precisely with Stop().

Gotcha: if arg or the cleanup closure references ptr, the object stays reachable and the cleanup never runs. Passing ptr itself as arg panics with "ptr is equal to arg, cleanup will never run". Cleanups are still not guaranteed to run at program exit.

The syscall calls above assume a Unix-like platform.

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions