# Modern Concurrency Patterns for Goroutines and Synchronization in Go

> Explore modern concurrency patterns for goroutines and synchronization in Go. Discover declarative patterns like wg.Go, sync.OnceFunc, and typed atomics to replace legacy code with safer, self-documenting APIs.

- Repository: [JetBrains/go-modern-guidelines](https://github.com/jetbrains/go-modern-guidelines)
- Tags: best-practices
- Published: 2026-08-29

---

**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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json), this pattern removes the need for explicit `wg.Add(1)` and `defer wg.Done()` scaffolding.

```go
// 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.

```go
// 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.

```go
// 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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json) explicitly recommend these over untyped `atomic.*` functions for improved readability and compile-time safety.

```go
// 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()`.

```go
// 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)`.

```go
// 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.

```go
// 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.