Fundamentals 12 min read

What’s New in Go’s sync Package? A Deep Dive into APIs, Performance, and Safety

Over the past two years, Go’s sync and sync/atomic packages have introduced new APIs like WaitGroup.Go, enhanced Map methods, modernized atomic types, restructured internal implementations, and added developer‑friendly safeguards such as noCopy, all aimed at improving usability, safety, and performance for concurrent Go programs.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
What’s New in Go’s sync Package? A Deep Dive into APIs, Performance, and Safety

1. New APIs and Feature Enhancements

To simplify common concurrency patterns, the sync package added several eagerly awaited APIs.

sync.WaitGroup.Go

Go 1.25 introduced the WaitGroup.Go method, which greatly simplifies launching goroutines within a WaitGroup. The old pattern required manually calling Add, launching a goroutine, and deferring Done():

wg.Add(1)
go func() {
    defer wg.Done()
    // ... do work ...
}()

The new pattern replaces that boilerplate with a single call:

wg.Go(func() {
    // ... do work ...
})

This helper also embeds the defer wg.Done() call, preventing the common mistake of forgetting to invoke Done().

Full example:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    urls := []string{
        "http://example.com",
        "http://example.org",
        "http://example.net",
    }

    for _, url := range urls {
        // Use wg.Go to start a goroutine
        wg.Go(func() {
            fmt.Printf("Fetching %s
", url)
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Fetched %s
", url)
        })
    }

    // Wait for all wg.Go‑started goroutines to finish
    wg.Wait()
    fmt.Println("All fetches completed.")
}

sync.Map.Clear and sync.Map.Swap

The sync.Map type also received useful new methods:

Clear() : removes all key‑value pairs from a Map in a single operation, providing an efficient way to empty a map (Go 1.23.0).

Swap() : atomically exchanges the old and new values for a given key (Go 1.20).

CompareAndSwap() / CompareAndDelete() : finer‑grained atomic operations that allow conditional swap or delete based on the previous value (Go 1.20).

Example:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store key‑value pairs
    m.Store("config", "v1")
    m.Store("feature_flag", "off")

    // Swap: atomically update "config" to "v2" and get the old value
    oldValue, loaded := m.Swap("config", "v2")
    if loaded {
        fmt.Printf("Swapped 'config'. Old value was: %s
", oldValue)
    }

    // Print current value
    currentValue, _ := m.Load("config")
    fmt.Printf("Current value of 'config' is: %s
", currentValue)

    // Clear: empty the entire map
    fmt.Println("
Clearing the map...")
    m.Clear()

    // Verify the map is empty
    m.Range(func(key, value interface{}) bool {
        fmt.Println("This should not be printed.")
        return true
    })
    fmt.Println("Map is empty.")
}

sync.Once Family Evolution

Go 1.21.0 introduced OnceFunc, OnceValue, and OnceValues, which reduce heap allocations and make lazy initialization or result caching more efficient. OnceFunc(f func()) func(): wraps a no‑argument, no‑return function so it executes only once. OnceValue[T any](f func() T) func() T: wraps a function that returns a single value; subsequent calls return the cached result. OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2): similar for functions returning two values.

Example using OnceValue:

package main

import (
    "fmt"
    "sync"
)

func main() {
    once := sync.OnceValue(func() int {
        sum := 0
        for i := 0; i < 1000; i++ {
            sum += i
        }
        fmt.Println("Computed once:", sum)
        return sum
    })
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            const want = 499500
            got := once()
            if got != want {
                fmt.Println("want", want, "got", got)
            }
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}

2. Performance Optimizations and Internal Refactoring

The performance of the sync package is critical, and recent releases have delivered optimizations in several areas.

sync.Map New Implementation

In Go 1.24 the internal implementation of sync.Map was rewritten to use a hash‑trie‑based design (HashTrieMap), aiming to improve performance across diverse concurrency scenarios.

sync.Mutex Internal Refactor

Go 1.24 added a HashTrieMap‑based implementation for Mutex. The older implementation is slated for removal in Go 1.26, after which sync.Map will also default to the HashTrieMap design. See the discussion at https://github.com/golang/go/issues/70683.

Atomic Optimizations in sync.Once

The Once.done field switched from atomic.Uint32 to atomic.Bool in Go 1.25, improving readability and type safety while reflecting a broader trend of replacing older atomic patterns with modern types.

3. Code Correctness and Developer Experience

To help developers write more robust concurrent code, the sync package has made extensive documentation and static‑analysis improvements.

Introduction of the noCopy Sentinel

Core types such as Mutex, RWMutex, WaitGroup, Cond, and Map now embed a non‑exported noCopy field. The go vet tool can detect accidental copying of these types, emitting warnings before compilation.

Incorrect usage example:

package main

import "sync"

// Counter is a counter protected by a lock
type Counter struct {
    sync.Mutex
    count int
}

// Inc increments the counter; using a value receiver copies the Mutex
func (c Counter) Inc() {
    // Note: copying the lock leads to a race
    c.Lock()
    c.count++
    c.Unlock()
}

func main() {
    var c Counter
    c.Inc()
}

Running go vet . reports:

main.go:12:6: call of method Inc copies lock value: main.Counter contains sync.Mutex

Continuous Documentation Improvements

Explicitly state that RWMutex read and write locks cannot be upgraded or downgraded.

Detail the concurrency guarantees and memory‑model behavior of Map.Range, Map.Delete, and related methods.

Provide clearer explanations for Cond.Wait semantics.

Link directly to the Go memory model from the package documentation.

4. Modernization of sync/atomic

The underlying sync/atomic package introduced generic, type‑safe atomic types in Go 1.19, such as atomic.Int64, atomic.Pointer[T], and atomic.Bool. Recent changes focus on encouraging the use of these new types and expanding the API.

Encouraging New Types : Legacy function‑style atomic operations like atomic.LoadUint32 are being replaced by methods on the new typed atoms, e.g., myAtomicBool.Load().

API Enrichment : New bit‑wise atomic functions And and Or have been added, with documentation guiding migration from older APIs.

Summary

In the past two years, Go’s sync package has evolved steadily toward greater usability, safety, and performance while preserving API stability. Helper functions like WaitGroup.Go reduce boilerplate, the noCopy sentinel and richer documentation raise code robustness, and internal performance rewrites and modern atomic types ensure the primitives meet growing demand, cementing Go’s position as a modern concurrent language.

PerformanceconcurrencyGoAPIsyncatomic
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

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.