Understanding `sync.OnceFunc` and `sync.OnceValue` in Go 1.21

sync.OnceFunc and sync.OnceValue are Go 1.21 standard library helpers that eliminate boilerplate by wrapping one-time initialization logic, providing thread-safe single execution and memoization without manual sync.Once variable management.

Go 1.21 expanded the sync package with utilities that modernize concurrent initialization patterns. According to the JetBrains go-modern-guidelines repository, these functions replace verbose var once sync.Once declarations with self-contained closures, reducing error-prone state management while maintaining identical thread-safety guarantees.

What Are sync.OnceFunc and sync.OnceValue?

These functions address distinct but related concurrency problems through simplified APIs.

sync.OnceFunc for Idempotent Execution

sync.OnceFunc accepts a nullary function and returns a new function that executes the original logic at most once, regardless of how many goroutines invoke it or how many times it is called. This eliminates package-level sync.Once variables when you need idempotent cleanup or initialization hooks.

sync.OnceValue for Thread-Safe Memoization

sync.OnceValue operates similarly but captures and returns a computed result. It accepts a parameterless function returning a value of type T, memoizes the result after the first invocation, and returns the cached value on subsequent calls. This removes the boilerplate of managing separate result variables alongside synchronization primitives.

Replacing Manual sync.Once Boilerplate

The traditional approach requires declaring a sync.Once variable separately from the function logic. The JetBrains guidelines explicitly recommend against this pattern. In internal/guidelines/guidelines.json (lines 997–1002), the repository states: "Use sync.OnceFunc instead of sync.Once plus a wrapper closure."

Consider the verbose manual approach:

var (
    once    sync.Once
    closeFn func()
)

func Close() {
    once.Do(func() {
        // expensive cleanup
        closeFn = func() { /* ... */ }
        closeFn()
    })
}

With sync.OnceFunc, this collapses into a single declaration as shown in internal/guidelines/guidelines.json (lines 1010–1015):

var closeOnce = sync.OnceFunc(func() {
    // ... perform expensive cleanup, close resources, etc.
    fmt.Println("cleanup executed")
})

func ShutDown() {
    closeOnce() // subsequent calls are no-ops
}

The returned function handles synchronization internally, removing the need for external state management while providing identical safety guarantees.

Memoizing Computed Values with sync.OnceValue

For expensive initialization that produces a value, sync.OnceValue provides type-safe memoization. The guidelines repository captures the recommendation at line 1020: "Use sync.OnceValue to memoize a computed value."

The following pattern from internal/guidelines/guidelines.json (lines 1035–1040) demonstrates loading configuration data:

func loadConfig() *Config {
    time.Sleep(2 * time.Second) // simulate work
    return &Config{Port: 8080}
}

// OnceValue memoizes the result of loadConfig.
var GetConfig = sync.OnceValue(loadConfig)

// Anywhere in the program:
func handler() {
    cfg := GetConfig() // loads once, then returns cached value
    fmt.Println("port:", cfg.Port)
}

This approach guarantees that loadConfig executes exactly once, even when GetConfig is invoked concurrently from multiple goroutines.

Key Implementation Files in the Repository

The authoritative definitions and recommendations reside in specific files within the JetBrains/go-modern-guidelines repository:

  • internal/guidelines/guidelines.json – Contains the canonical recommendations at lines 997–1002 and 1020–1035, including the specific migration rules for OnceFunc and OnceValue.
  • internal/guidelines/guidelines.go – Loads and formats the JSON guidelines for CLI consumption, demonstrating how the tooling processes these recommendations programmatically.
  • FEATURES.md – Tracks the migration status for standard library features, confirming the adoption path for Go 1.21's synchronization helpers.

Summary

  • sync.OnceFunc wraps a function to ensure it executes at most once, replacing manual sync.Once variables and associated closures.
  • sync.OnceValue memoizes a function's return value with built-in thread safety, eliminating separate result variables and getter boilerplate.
  • Both utilities were introduced in Go 1.21 and provide the same synchronization guarantees as sync.Once with improved readability.
  • The JetBrains go-modern-guidelines repository explicitly recommends these helpers over legacy patterns in internal/guidelines/guidelines.json.

Frequently Asked Questions

When should I use sync.OnceFunc instead of sync.Once?

Use sync.OnceFunc when you need a standalone function that executes exactly once and can be called multiple times safely without maintaining a separate sync.Once variable. This simplifies code organization for one-time setup or cleanup routines.

Does sync.OnceValue handle panics in the wrapped function?

Yes, if the wrapped function panics during the first call, sync.OnceValue considers the function completed and will not rerun it. Subsequent calls return the zero value of the type and do not panic again, similar to sync.Once behavior.

Can I use these functions in Go versions earlier than 1.21?

No, sync.OnceFunc and sync.OnceValue require Go 1.21 or later. If you must support earlier versions, continue using the traditional sync.Once pattern with external state management.

Are there performance differences between manual sync.Once and these helpers?

No, the generated code provides equivalent performance characteristics. The helpers compile down to efficient synchronization primitives without additional overhead compared to manually implemented patterns.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →