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
- Q353How do you do zero-copy
[]byte↔stringconversion correctly, and what can go wrong? - Q354What does this program print? Explain how finalizers behave.
- Q356What are weak pointers in Go and when would you use them?
- Q357What does
runtime.KeepAlivedo and when is it required? - Q358What does this print, and what memory problem does it illustrate?
- Q359Why can removing elements from a slice of pointers leak memory?