Master Go’s New Cleanup Functions and Weak Pointers for Safer Memory Management
Go 1.24 introduces runtime.AddCleanup and weak.Pointer, advanced garbage‑collector tools that let developers attach cleanup callbacks without retaining objects and safely cache objects via weak references, offering more precise memory management, eliminating finalizer pitfalls, and enabling custom unique‑package implementations.
Original article: "From unique to cleanups and weak: new low‑level tools for efficiency" Link: https://go.dev/blog/cleanups-and-weak Translation by DeepSeek
Michael Knyszek – March 6, 2025
In last year’s post about the unique package we previewed several features that were then under proposal review; they are now available to all developers starting with Go 1.24. The new features are the runtime.AddCleanup function (queues a function to run when an object becomes unreachable) and the weak.Pointer type (a safe reference to an object that does not prevent its garbage collection). Together they enable you to build your own unique package.
Note: these features are advanced garbage‑collector capabilities. If you are not familiar with basic GC concepts, first read the introductory part of the Garbage Collection Guide .
Cleanups (Cleanup Functions)
If you have used finalizers, the concept of a cleanup should be familiar. A finalizer is a function attached to an allocated object via runtime.SetFinalizer that the garbage collector calls after the object becomes unreachable. A cleanup works in a similar high‑level way.
Below is an example that uses a memory‑mapped file to illustrate the use of a cleanup:
//go:build unix
type MemoryMappedFile struct {
data []byte
}
func NewMemoryMappedFile(filename string) (*MemoryMappedFile, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
// Get file info (need file size)
fi, err := f.Stat()
if err != nil {
return nil, err
}
// Extract file descriptor
conn, err := f.SyscallConn()
if err != nil {
return nil, err
}
var data []byte
connErr := conn.Control(func(fd uintptr) {
// Create memory mapping backed by the file
data, err = syscall.Mmap(int(fd), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
})
if connErr != nil {
return nil, connErr
}
if err != nil {
return nil, err
}
mf := &MemoryMappedFile{data: data}
cleanup := func(data []byte) {
syscall.Munmap(data) // ignore error
}
runtime.AddCleanup(mf, cleanup, data)
return mf, nil
}The content of the memory‑mapped file is directly mapped into memory. When the *MemoryMappedFile is no longer referenced, the mapping is automatically cleaned up.
The three parameters of runtime.AddCleanup are:
The address of the variable to which the cleanup is attached.
The cleanup function itself.
The arguments passed to the cleanup function.
The key difference from runtime.SetFinalizer is that the cleanup function’s arguments are independent of the attached object, fixing several issues with finalizers.
Finalizer pain points include:
Reference cycles can cause memory leaks.
At least two full GC cycles are needed to reclaim memory.
Object resurrection problems.
Cleanups solve these problems by not passing the original object:
Objects involved in reference cycles can still be reclaimed.
Memory can be reclaimed immediately.
Weak Pointers
Suppose we need to deduplicate memory‑mapped files by filename. The weak.Pointer type can safely implement a cache:
var cache sync.Map // map[string]weak.Pointer[MemoryMappedFile]
func NewCachedMemoryMappedFile(filename string) (*MemoryMappedFile, error) {
var newFile *MemoryMappedFile
for {
// Try to load from cache
value, ok := cache.Load(filename)
if !ok {
// Create new mapping if needed
if newFile == nil {
var err error
newFile, err = NewMemoryMappedFile(filename)
if err != nil {
return nil, err
}
}
// Attempt to store new mapping
wp := weak.Make(newFile)
var loaded bool
value, loaded = cache.LoadOrStore(filename, wp)
if !loaded {
runtime.AddCleanup(newFile, func(filename string) {
cache.CompareAndDelete(filename, wp)
}, filename)
return newFile, nil
}
}
// Check validity of cached entry
if mf := value.(weak.Pointer[MemoryMappedFile]).Value(); mf != nil {
return mf, nil
}
// Found stale entry, delete it
cache.CompareAndDelete(filename, value)
}
}Key features demonstrated:
Weak pointers are comparable and have stable identity.
Multiple independent cleanups can be attached to a single object.
Enables a generic cache structure (see original generic Cache example).
Considerations and Future Work
When using these features, keep in mind:
The object associated with a cleanup must not be referenced by the cleanup function or its arguments.
If a weak pointer is used as a map key, the value must not reference the key object.
Non‑deterministic behavior depends on GC implementation details.
Testing is challenging.
Possible future improvements:
Support for Ephemeron (short‑lived objects).
APIs to directly track mapped memory regions.
Summary
The runtime.AddCleanup and weak.Pointer features give Go finer‑grained memory‑management capabilities, but they must be used cautiously. Most scenarios should rely on higher‑level standard‑library abstractions rather than direct manipulation. Developers are encouraged to read the updated Garbage Collection Guide for recommended usage.
These additions show the Go team’s commitment to preserving language simplicity while providing essential low‑level support for advanced use cases; correctly applied, they solve problems that were previously hard to handle.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiao Lou's Tech Notes
Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices
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.
