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.
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.MutexContinuous 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.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
