Fundamentals 9 min read

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.

Xiao Lou's Tech Notes
Xiao Lou's Tech Notes
Xiao Lou's Tech Notes
Master Go’s New Cleanup Functions and Weak Pointers for Safer Memory Management
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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

weak referencesweak.Pointercleanup functionsruntime.AddCleanup
Xiao Lou's Tech Notes
Written by

Xiao Lou's Tech Notes

Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.