Modern Concurrency Patterns for Goroutines and Synchronization in Go

The JetBrains/go-modern-guidelines repository recommends seven declarative patterns—including wg.Go, sync.OnceFunc, typed atomics, and context causes—that eliminate manual sync.WaitGroup bookkeeping and replace legacy synchronization boilerplate with safer, self-documenting standard library APIs.

The JetBrains/go-modern-guidelines project captures idiomatic approaches to concurrent programming using Go 1.19+ standard library enhancements. These modern concurrency patterns for goroutines and synchronization in Go replace error-prone manual operations with declarative APIs, reducing cognitive load and preventing common race conditions.

Modern WaitGroup Patterns: wg.Go

The wg.Go method streamlines goroutine lifecycle tracking by automatically handling Add and Done calls internally. According to the guidelines defined in internal/guidelines/guidelines.json, this pattern removes the need for explicit wg.Add(1) and defer wg.Done() scaffolding.

// Before: Manual Add/Done bookkeeping
var wg sync.WaitGroup
for _, item := range items {
    wg.Add(1)
    go func() {
        defer wg.Done()
        process(item)
    }()
}
wg.Wait()

// After: Declarative goroutine spawning
var wg sync.WaitGroup
for _, item := range items {
    wg.Go(func() { process(item) })
}
wg.Wait()

One-Time Execution and Memoization

Go 1.21+ introduces sync.OnceFunc and sync.OnceValue to simplify single-execution semantics and lazy initialization.

sync.OnceFunc for Cleanup Operations

This helper creates a function that executes at most once, eliminating the separate sync.Once variable and wrapper closure pattern. As documented in the repository's guideline entries, this reduces boilerplate when implementing shutdown or cleanup hooks.

// Before: Manual sync.Once management
var once sync.Once
cleanup := func() {
    once.Do(func() { close(ch) })
}

// After: Declarative once semantics
cleanup := sync.OnceFunc(func() { close(ch) })

sync.OnceValue for Lazy Initialization

Use sync.OnceValue to memoize computed values without managing a separate result variable and sync.Once instance. The pattern ensures thread-safe lazy evaluation with minimal syntax.

// Before: Separate once variable and value storage
var once sync.Once
var value T
getter := func() T {
    once.Do(func() { value = computeValue() })
    return value
}

// After: Encapsulated lazy computation
getter := sync.OnceValue(func() T { return computeValue() })

Type-Safe Atomic Operations

Go 1.19 introduced typed atomic wrappers (atomic.Bool, atomic.Int64, atomic.Pointer[T]) that bind value types to atomic operations. The guidelines in internal/guidelines/guidelines.json explicitly recommend these over untyped atomic.* functions for improved readability and compile-time safety.

// Before: Untyped atomics with manual casting
var enabled int32
atomic.StoreInt32(&enabled, 1)
if atomic.LoadInt32(&enabled) != 0 { run() }

// After: Type-safe atomic operations
var enabled atomic.Bool
enabled.Store(true)
if enabled.Load() { run() }

Context-Aware Lifecycle Management

Modern context patterns enable declarative cleanup and rich cancellation diagnostics without spawning dedicated goroutines.

context.AfterFunc for Cleanup

The context.AfterFunc method registers cleanup work that executes when a context is cancelled, returning a stopper function for cancellation. This pattern avoids allocating a goroutine solely to wait on ctx.Done().

// Before: Dedicated goroutine for cancellation waiting
go func() {
    <-ctx.Done()
    cleanup()
}()

// After: Registered callback with explicit stop control
stop := context.AfterFunc(ctx, cleanup)
defer stop()

Cancellation with Cause

Use context.WithCancelCause to attach specific error information to cancellation signals. This allows downstream consumers to determine why a context was cancelled using context.Cause(ctx).

// Before: Opaque cancellation
ctx, cancel := context.WithCancel(parent)
cancel() // no cause provided

// After: Rich diagnostic information
ctx, cancel := context.WithCancelCause(parent)
cancel(errSpecificCause)      // attach the reason
cause := context.Cause(ctx)   // retrieve it later for logging/metrics

Timeout and Deadline with Cause

Similarly, context.WithTimeoutCause and context.WithDeadlineCause extend deadline-based contexts with diagnostic causes, enabling clearer error tracing in distributed systems.

// Before: Standard timeout without context
ctx, cancel := context.WithTimeout(parent, d)
defer cancel()

// After: Timeout with attached cause for debugging
ctx, cancel := context.WithTimeoutCause(parent, d, errTimeout)
defer cancel()

Summary

  • wg.Go eliminates manual sync.WaitGroup bookkeeping by automatically managing Add and Done calls when spawning goroutines.
  • sync.OnceFunc and sync.OnceValue provide declarative APIs for one-time execution and lazy memoization without separate variable management.
  • Typed atomics (atomic.Bool, atomic.Int64, atomic.Pointer[T]) replace untyped atomic functions with type-safe alternatives introduced in Go 1.19.
  • context.AfterFunc registers cleanup callbacks without spawning dedicated goroutines, improving resource efficiency.
  • Cancellation with cause patterns (WithCancelCause, WithTimeoutCause, WithDeadlineCause) attach diagnostic error information to context cancellations for better observability.

Frequently Asked Questions

What is wg.Go and when should I use it?

wg.Go is a method on sync.WaitGroup that automatically increments the counter before spawning a goroutine and decrements it when the function returns. Use it whenever you need to track multiple concurrent operations to completion, as it eliminates the boilerplate of manual Add, Done, and defer statements.

How do typed atomics improve code safety compared to sync/atomic functions?

Typed atomics such as atomic.Bool and atomic.Int64 bind the value's type to the atomic operations at compile time, preventing incorrect size assumptions and casting errors. According to the JetBrains/go-modern-guidelines source, they improve readability by making the atomic intent explicit and eliminating the need for unsafe.Pointer conversions or manual memory alignment concerns.

What is the difference between context.WithCancel and context.WithCancelCause?

context.WithCancel returns a cancellation function that accepts no arguments, providing no information about why cancellation occurred. context.WithCancelCause returns a cancellation function that accepts an error parameter, allowing you to attach a specific cause that downstream code can retrieve via context.Cause(ctx) for logging, metrics, or error propagation decisions.

When should I use context.AfterFunc instead of a goroutine with <-ctx.Done()?

Use context.AfterFunc when you need to execute cleanup logic upon context cancellation but don't need a long-running goroutine for other purposes. This pattern is more resource-efficient because it avoids allocating a dedicated goroutine that only waits on the Done channel, instead using the runtime's internal event notification system.

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 →